summaryrefslogtreecommitdiff
path: root/deps/v8/build/android/pylib/symbols/symbol_utils.py
blob: e4e3faac8036b1cfaf80fb96d6f618f1fbfdc6f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import bisect
import collections
import logging
import os
import re

from pylib.constants import host_paths
from pylib.symbols import elf_symbolizer


def _AndroidAbiToCpuArch(android_abi):
  """Return the Chromium CPU architecture name for a given Android ABI."""
  _ARCH_MAP = {
    'armeabi': 'arm',
    'armeabi-v7a': 'arm',
    'arm64-v8a': 'arm64',
    'x86_64': 'x64',
  }
  return _ARCH_MAP.get(android_abi, android_abi)


def _HexAddressRegexpFor(android_abi):
  """Return a regexp matching hexadecimal addresses for a given Android ABI."""
  if android_abi in ['x86_64', 'arm64-v8a', 'mips64']:
    width = 16
  else:
    width = 8
  return '[0-9a-f]{%d}' % width


class HostLibraryFinder(object):
  """Translate device library path to matching host unstripped library path.

  Usage is the following:
    1) Create instance.
    2) Call AddSearchDir() once or more times to add host directory path to
       look for unstripped native libraries.
    3) Call Find(device_libpath) repeatedly to translate a device-specific
       library path into the corresponding host path to the unstripped
       version.
  """
  def __init__(self):
    """Initialize instance."""
    self._search_dirs = []
    self._lib_map = {}        # Map of library name to host file paths.

  def AddSearchDir(self, lib_dir):
    """Add a directory to the search path for host native shared libraries.

    Args:
      lib_dir: host path containing native libraries.
    """
    if not os.path.exists(lib_dir):
      logging.warning('Ignoring missing host library directory: %s', lib_dir)
      return
    if not os.path.isdir(lib_dir):
      logging.warning('Ignoring invalid host library directory: %s', lib_dir)
      return
    self._search_dirs.append(lib_dir)
    self._lib_map = {}  # Reset the map.

  def Find(self, device_libpath):
    """Find the host file path matching a specific device library path.

    Args:
      device_libpath: device-specific file path to library or executable.
    Returns:
      host file path to the unstripped version of the library, or None.
    """
    host_lib_path = None
    lib_name = os.path.basename(device_libpath)
    host_lib_path = self._lib_map.get(lib_name)
    if not host_lib_path:
      for search_dir in self._search_dirs:
        lib_path = os.path.join(search_dir, lib_name)
        if os.path.exists(lib_path):
          host_lib_path = lib_path
          break

      if not host_lib_path:
        logging.debug('Could not find host library for: %s', lib_name)
      self._lib_map[lib_name] = host_lib_path

    return host_lib_path



class SymbolResolver(object):
  """A base class for objets that can symbolize library (path, offset)
     pairs into symbol information strings. Usage is the following:

     1) Create new instance (by calling the constructor of a derived
        class, since this is only the base one).

     2) Call SetAndroidAbi() before any call to FindSymbolInfo() in order
        to set the Android CPU ABI used for symbolization.

     3) Before the first call to FindSymbolInfo(), one can call
        AddLibraryOffset(), or AddLibraryOffsets() to record a set of offsets
        that you will want to symbolize later through FindSymbolInfo(). Doing
        so allows some SymbolResolver derived classes to work faster (e.g. the
        one that invokes the 'addr2line' program, since the latter works faster
        if the offsets provided as inputs are sorted in increasing order).

     3) Call FindSymbolInfo(path, offset) to return the corresponding
        symbol information string, or None if this doesn't correspond
        to anything the instance can handle.

        Note that whether the path is specific to the device or to the
        host depends on the derived class implementation.
  """
  def __init__(self):
    self._android_abi = None
    self._lib_offsets_map = collections.defaultdict(set)

  def SetAndroidAbi(self, android_abi):
    """Set the Android ABI value for this instance.

    Calling this function before FindSymbolInfo() is required by some
    derived class implementations.

    Args:
      android_abi: Native Android CPU ABI name (e.g. 'armeabi-v7a').
    Raises:
      Exception if the ABI was already set with a different value.
    """
    if self._android_abi and self._android_abi != android_abi:
      raise Exception('Cannot reset Android ABI to new value %s, already set '
                      'to %s' % (android_abi, self._android_abi))

    self._android_abi = android_abi

  def AddLibraryOffset(self, lib_path, offset):
    """Associate a single offset to a given device library.

    This must be called before FindSymbolInfo(), otherwise its input arguments
    will be ignored.

    Args:
      lib_path: A library path.
      offset: An integer offset within the corresponding library that will be
        symbolized by future calls to FindSymbolInfo.
    """
    self._lib_offsets_map[lib_path].add(offset)

  def AddLibraryOffsets(self, lib_path, lib_offsets):
    """Associate a set of wanted offsets to a given device library.

    This must be called before FindSymbolInfo(), otherwise its input arguments
    will be ignored.

    Args:
      lib_path: A library path.
      lib_offsets: An iterable of integer offsets within the corresponding
        library that will be symbolized by future calls to FindSymbolInfo.
    """
    self._lib_offsets_map[lib_path].update(lib_offsets)

  # pylint: disable=unused-argument,no-self-use
  def FindSymbolInfo(self, lib_path, lib_offset):
    """Symbolize a device library path and offset.

    Args:
      lib_path: Library path (device or host specific, depending on the
        derived class implementation).
      lib_offset: Integer offset within the library.
    Returns:
      Corresponding symbol information string, or None.
    """
    # The base implementation cannot symbolize anything.
    return None
  # pylint: enable=unused-argument,no-self-use


class ElfSymbolResolver(SymbolResolver):
  """A SymbolResolver that can symbolize host path + offset values using
     an elf_symbolizer.ELFSymbolizer instance.
  """
  def __init__(self, addr2line_path_for_tests=None):
    super(ElfSymbolResolver, self).__init__()
    self._addr2line_path = addr2line_path_for_tests

    # Used to cache one ELFSymbolizer instance per library path.
    self._elf_symbolizer_cache = {}

    # Used to cache FindSymbolInfo() results. Maps host library paths
    # to (offset -> symbol info string) dictionaries.
    self._symbol_info_cache = collections.defaultdict(dict)
    self._allow_symbolizer = True

  def _CreateSymbolizerFor(self, host_path):
    """Create the ELFSymbolizer instance associated with a given lib path."""
    addr2line_path = self._addr2line_path
    if not addr2line_path:
      if not self._android_abi:
        raise Exception(
            'Android CPU ABI must be set before calling FindSymbolInfo!')

      cpu_arch = _AndroidAbiToCpuArch(self._android_abi)
      self._addr2line_path = host_paths.ToolPath('addr2line', cpu_arch)

    return elf_symbolizer.ELFSymbolizer(
        elf_file_path=host_path, addr2line_path=self._addr2line_path,
        callback=ElfSymbolResolver._Callback, inlines=True)

  def DisallowSymbolizerForTesting(self):
    """Disallow FindSymbolInfo() from using a symbolizer.

    This is used during unit-testing to ensure that the offsets that were
    recorded via AddLibraryOffset()/AddLibraryOffsets() are properly
    symbolized, but not anything else.
    """
    self._allow_symbolizer = False

  def FindSymbolInfo(self, host_path, offset):
    """Override SymbolResolver.FindSymbolInfo.

    Args:
      host_path: Host-specific path to the native shared library.
      offset: Integer offset within the native library.
    Returns:
      A symbol info string, or None.
    """
    offset_map = self._symbol_info_cache[host_path]
    symbol_info = offset_map.get(offset)
    if symbol_info:
      return symbol_info

    # Create symbolizer on demand.
    symbolizer = self._elf_symbolizer_cache.get(host_path)
    if not symbolizer:
      symbolizer = self._CreateSymbolizerFor(host_path)
      self._elf_symbolizer_cache[host_path] = symbolizer

      # If there are pre-recorded offsets for this path, symbolize them now.
      offsets = self._lib_offsets_map.get(host_path)
      if offsets:
        offset_map = {}
        for pre_offset in offsets:
          symbolizer.SymbolizeAsync(
              pre_offset, callback_arg=(offset_map, pre_offset))
        symbolizer.WaitForIdle()
        self._symbol_info_cache[host_path] = offset_map

        symbol_info = offset_map.get(offset)
        if symbol_info:
          return symbol_info

    if not self._allow_symbolizer:
      return None

    # Symbolize single offset. Slower if addresses are not provided in
    # increasing order to addr2line.
    symbolizer.SymbolizeAsync(offset,
                              callback_arg=(offset_map, offset))
    symbolizer.WaitForIdle()
    return offset_map.get(offset)

  @staticmethod
  def _Callback(sym_info, callback_arg):
    offset_map, offset = callback_arg
    offset_map[offset] = str(sym_info)


class DeviceSymbolResolver(SymbolResolver):
  """A SymbolResolver instance that accepts device-specific path.

  Usage is the following:
    1) Create new instance, passing a parent SymbolResolver instance that
       accepts host-specific paths, and a HostLibraryFinder instance.

    2) Optional: call AddApkOffsets() to add offsets from within an APK
       that contains uncompressed native shared libraries.

    3) Use it as any SymbolResolver instance.
  """
  def __init__(self, host_resolver, host_lib_finder):
    """Initialize instance.

    Args:
      host_resolver: A parent SymbolResolver instance that will be used
        to resolve symbols from host library paths.
      host_lib_finder: A HostLibraryFinder instance used to locate
        unstripped libraries on the host.
    """
    super(DeviceSymbolResolver, self).__init__()
    self._host_lib_finder = host_lib_finder
    self._bad_device_lib_paths = set()
    self._host_resolver = host_resolver

  def SetAndroidAbi(self, android_abi):
    super(DeviceSymbolResolver, self).SetAndroidAbi(android_abi)
    self._host_resolver.SetAndroidAbi(android_abi)

  def AddLibraryOffsets(self, device_lib_path, lib_offsets):
    """Associate a set of wanted offsets to a given device library.

    This must be called before FindSymbolInfo(), otherwise its input arguments
    will be ignored.

    Args:
      device_lib_path: A device-specific library path.
      lib_offsets: An iterable of integer offsets within the corresponding
        library that will be symbolized by future calls to FindSymbolInfo.
        want to symbolize.
    """
    if device_lib_path in self._bad_device_lib_paths:
      return

    host_lib_path = self._host_lib_finder.Find(device_lib_path)
    if not host_lib_path:
      # NOTE: self._bad_device_lib_paths is only used to only print this
      #       warning once per bad library.
      logging.warning('Could not find host library matching device path: %s',
                      device_lib_path)
      self._bad_device_lib_paths.add(device_lib_path)
      return

    self._host_resolver.AddLibraryOffsets(host_lib_path, lib_offsets)

  def AddApkOffsets(self, device_apk_path, apk_offsets, apk_translator):
    """Associate a set of wanted offsets to a given device APK path.

    This converts the APK-relative offsets into offsets relative to the
    uncompressed libraries it contains, then calls AddLibraryOffsets()
    for each one of the libraries.

    Must be called before FindSymbolInfo() as well, otherwise input arguments
    will be ignored.

    Args:
      device_apk_path: Device-specific APK path.
      apk_offsets: Iterable of offsets within the APK file.
      apk_translator: An ApkLibraryPathTranslator instance used to extract
        library paths from the APK.
    """
    libraries_map = collections.defaultdict(set)
    for offset in apk_offsets:
      lib_path, lib_offset = apk_translator.TranslatePath(device_apk_path,
                                                          offset)
      libraries_map[lib_path].add(lib_offset)

    for lib_path, lib_offsets in libraries_map.iteritems():
      self.AddLibraryOffsets(lib_path, lib_offsets)

  def FindSymbolInfo(self, device_path, offset):
    """Overrides SymbolResolver.FindSymbolInfo.

    Args:
      device_path: Device-specific library path (e.g.
        '/data/app/com.example.app-1/lib/x86/libfoo.so')
      offset: Offset in device library path.
    Returns:
      Corresponding symbol information string, or None.
    """
    host_path = self._host_lib_finder.Find(device_path)
    if not host_path:
      return None

    return self._host_resolver.FindSymbolInfo(host_path, offset)


class MemoryMap(object):
  """Models the memory map of a given process. Usage is:

    1) Create new instance, passing the Android ABI.

    2) Call TranslateLine() whenever you want to detect and translate any
       memory map input line.

    3) Otherwise, it is possible to parse the whole memory map input with
       ParseLines(), then call FindSectionForAddress() repeatedly in order
       to translate a memory address into the corresponding mapping and
       file information tuple (e.g. to symbolize stack entries).
  """

  # A named tuple describing interesting memory map line items.
  # Fields:
  #   addr_start: Mapping start address in memory.
  #   file_offset: Corresponding file offset.
  #   file_size: Corresponding mapping size in bytes.
  #   file_path: Input file path.
  #   match: Corresponding regular expression match object.
  LineTuple = collections.namedtuple('MemoryMapLineTuple',
                                     'addr_start,file_offset,file_size,'
                                     'file_path, match')

  # A name tuple describing a memory map section.
  # Fields:
  #   address: Memory address.
  #   size: Size in bytes in memory
  #   offset: Starting file offset.
  #   path: Input file path.
  SectionTuple = collections.namedtuple('MemoryMapSection',
                                        'address,size,offset,path')

  def __init__(self, android_abi):
    """Initializes instance.

    Args:
      android_abi: Android CPU ABI name (e.g. 'armeabi-v7a')
    """
    hex_addr = _HexAddressRegexpFor(android_abi)

    # pylint: disable=line-too-long
    # A regular expression used to match memory map entries which look like:
    #    b278c000-b2790fff r--   4fda000      5000  /data/app/com.google.android.apps.chrome-2/base.apk
    # pylint: enable=line-too-long
    self._re_map_section = re.compile(
        r'\s*(?P<addr_start>' + hex_addr + r')-(?P<addr_end>' + hex_addr + ')' +
        r'\s+' +
        r'(?P<perm>...)\s+' +
        r'(?P<file_offset>[0-9a-f]+)\s+' +
        r'(?P<file_size>[0-9a-f]+)\s*' +
        r'(?P<file_path>[^ \t]+)?')

    self._addr_map = []  # Sorted list of (address, size, path, offset) tuples.
    self._sorted_addresses = []  # Sorted list of address fields in _addr_map.
    self._in_section = False

  def TranslateLine(self, line, apk_path_translator):
    """Try to translate a memory map input line, if detected.

    This only takes care of converting mapped APK file path and offsets
    into a corresponding uncompressed native library file path + new offsets,
    e.g. '..... <offset> <size> /data/.../base.apk' gets
    translated into '.... <new-offset> <size> /data/.../base.apk!lib/libfoo.so'

    This function should always work, even if ParseLines() was not called
    previously.

    Args:
      line: Input memory map / tombstone line.
      apk_translator: An ApkLibraryPathTranslator instance, used to map
        APK offsets into uncompressed native libraries + new offsets.
    Returns:
      Translated memory map line, if relevant, or unchanged input line
      otherwise.
    """
    t = self._ParseLine(line.rstrip())
    if not t:
      return line

    new_path, new_offset = apk_path_translator.TranslatePath(
        t.file_path, t.file_offset)

    if new_path == t.file_path:
      return line

    pos = t.match.start('file_path')
    return '%s%s (offset 0x%x)%s' % (line[0:pos], new_path, new_offset,
                                     line[t.match.end('file_path'):])

  def ParseLines(self, input_lines, in_section=False):
    """Parse a list of input lines and extract the APK memory map out of it.

    Args:
      input_lines: list, or iterable, of input lines.
      in_section: Optional. If true, considers that the input lines are
        already part of the memory map. Otherwise, wait until the start of
        the section appears in the input before trying to record data.
    Returns:
      True iff APK-related memory map entries were found. False otherwise.
    """
    addr_list = []  # list of (address, size, file_path, file_offset) tuples.
    self._in_section = in_section
    for line in input_lines:
      t = self._ParseLine(line.rstrip())
      if not t:
        continue

      addr_list.append(t)

    self._addr_map = sorted(addr_list,
                            lambda x, y: cmp(x.addr_start, y.addr_start))
    self._sorted_addresses = [e.addr_start for e in self._addr_map]
    return bool(self._addr_map)

  def _ParseLine(self, line):
    """Used internally to recognized memory map input lines.

    Args:
      line: Input logcat or tomstone line.
    Returns:
      A LineTuple instance on success, or None on failure.
    """
    if not self._in_section:
      self._in_section = line.startswith('memory map:')
      return None

    m = self._re_map_section.match(line)
    if not m:
      self._in_section = False  # End of memory map section
      return None

    # Only accept .apk and .so files that are not from the system partitions.
    file_path = m.group('file_path')
    if not file_path:
      return None

    if file_path.startswith('/system') or file_path.startswith('/vendor'):
      return None

    if not (file_path.endswith('.apk') or file_path.endswith('.so')):
      return None

    addr_start = int(m.group('addr_start'), 16)
    file_offset = int(m.group('file_offset'), 16)
    file_size = int(m.group('file_size'), 16)

    return self.LineTuple(addr_start, file_offset, file_size, file_path, m)

  def Dump(self):
    """Print memory map for debugging."""
    print 'MEMORY MAP ['
    for t in self._addr_map:
      print '[%08x-%08x %08x %08x %s]' % (
          t.addr_start, t.addr_start + t.file_size, t.file_size, t.file_offset,
          t.file_path)
    print '] MEMORY MAP'

  def FindSectionForAddress(self, addr):
    """Find the map section corresponding to a specific memory address.

    Call this method only after using ParseLines() was called to extract
    relevant information from the memory map.

    Args:
      addr: Memory address
    Returns:
      A SectionTuple instance on success, or None on failure.
    """
    pos = bisect.bisect_right(self._sorted_addresses, addr)
    if pos > 0:
      # All values in [0,pos) are <= addr, just ensure that the last
      # one contains the address as well.
      entry = self._addr_map[pos - 1]
      if entry.addr_start + entry.file_size > addr:
        return self.SectionTuple(entry.addr_start, entry.file_size,
                                 entry.file_offset, entry.file_path)
    return None


class BacktraceTranslator(object):
  """Translates backtrace-related lines in a tombstone or crash report.

  Usage is the following:
    1) Create new instance with appropriate arguments.
    2) If the tombstone / logcat input is available, one can call
       FindLibraryOffsets() in order to detect which library offsets
       will need to be symbolized during a future parse. Doing so helps
       speed up the ELF symbolizer.
    3) For each tombstone/logcat input line, call TranslateLine() to
       try to detect and symbolize backtrace lines.
  """

  # A named tuple for relevant input backtrace lines.
  # Fields:
  #   rel_pc: Instruction pointer, relative to offset in library start.
  #   location: Library or APK file path.
  #   offset: Load base of executable code in library or apk file path.
  #   match: The corresponding regular expression match object.
  # Note:
  #   The actual instruction pointer always matches the position at
  #   |offset + rel_pc| in |location|.
  LineTuple = collections.namedtuple('BacktraceLineTuple',
                                      'rel_pc,location,offset,match')

  def __init__(self, android_abi, apk_translator):
    """Initialize instance.

    Args:
      android_abi: Android CPU ABI name (e.g. 'armeabi-v7a').
      apk_translator: ApkLibraryPathTranslator instance used to convert
        mapped APK file offsets into uncompressed library file paths with
        new offsets.
    """
    hex_addr = _HexAddressRegexpFor(android_abi)

    # A regular expression used to match backtrace lines.
    self._re_backtrace = re.compile(
        r'.*#(?P<frame>[0-9]{2})\s+' +
        r'(..)\s+' +
        r'(?P<rel_pc>' + hex_addr + r')\s+' +
        r'(?P<location>[^ \t]+)' +
        r'(\s+\(offset 0x(?P<offset>[0-9a-f]+)\))?')

    # In certain cases, offset will be provided as <location>+0x<offset>
    # instead of <location> (offset 0x<offset>). This is a regexp to detect
    # this.
    self._re_location_offset = re.compile(
        r'.*\+0x(?P<offset>[0-9a-f]+)$')

    self._apk_translator = apk_translator
    self._in_section = False

  def _ParseLine(self, line):
    """Used internally to detect and decompose backtrace input lines.

    Args:
      line: input tombstone line.
    Returns:
      A LineTuple instance on success, None on failure.
    """
    if not self._in_section:
      self._in_section = line.startswith('backtrace:')
      return None

    line = line.rstrip()
    m = self._re_backtrace.match(line)
    if not m:
      self._in_section = False
      return None

    location = m.group('location')
    offset = m.group('offset')
    if not offset:
      m2 = self._re_location_offset.match(location)
      if m2:
        offset = m2.group('offset')
        location = location[0:m2.start('offset') - 3]

    if not offset:
      return None

    offset = int(offset, 16)
    rel_pc = int(m.group('rel_pc'), 16)

    # Two cases to consider here:
    #
    # * If this is a library file directly mapped in memory, then |rel_pc|
    #   if the direct offset within the library, and doesn't need any kind
    #   of adjustement.
    #
    # * If this is a library mapped directly from an .apk file, then
    #   |rel_pc| is the offset in the APK, and |offset| happens to be the
    #   load base of the corresponding library.
    #
    if location.endswith('.so'):
      # For a native library directly mapped from the file system,
      return self.LineTuple(rel_pc, location, offset, m)

    if location.endswith('.apk'):
      # For a native library inside an memory-mapped APK file,
      new_location, new_offset = self._apk_translator.TranslatePath(
          location, offset)

      return self.LineTuple(rel_pc, new_location, new_offset, m)

    # Ignore anything else (e.g. .oat or .odex files).
    return None

  def FindLibraryOffsets(self, input_lines, in_section=False):
    """Parse a tombstone's backtrace section and find all library offsets in it.

    Args:
      input_lines: List or iterables of intput tombstone lines.
      in_section: Optional. If True, considers that the stack section has
        already started.
    Returns:
      A dictionary mapping device library paths to sets of offsets within
      then.
    """
    self._in_section = in_section
    result = collections.defaultdict(set)
    for line in input_lines:
      t = self._ParseLine(line)
      if not t:
        continue

      result[t.location].add(t.offset + t.rel_pc)
    return result

  def TranslateLine(self, line, symbol_resolver):
    """Symbolize backtrace line if recognized.

    Args:
      line: input backtrace line.
      symbol_resolver: symbol resolver instance to use. This method will
        call its FindSymbolInfo(device_lib_path, lib_offset) method to
        convert offsets into symbol informations strings.
    Returns:
      Translated line (unchanged if not recognized as a back trace).
    """
    t = self._ParseLine(line)
    if not t:
      return line

    symbol_info = symbol_resolver.FindSymbolInfo(t.location,
                                                 t.offset + t.rel_pc)
    if not symbol_info:
      symbol_info = 'offset 0x%x' % t.offset

    pos = t.match.start('location')
    pos2 = t.match.end('offset') + 1
    if pos2 <= 0:
      pos2 = t.match.end('location')
    return '%s%s (%s)%s' % (line[:pos], t.location, symbol_info, line[pos2:])


class StackTranslator(object):
  """Translates stack-related lines in a tombstone or crash report."""

  # A named tuple describing relevant stack input lines.
  # Fields:
  #  address: Address as it appears in the stack.
  #  lib_path: Library path where |address| is mapped.
  #  lib_offset: Library load base offset. for |lib_path|.
  #  match: Corresponding regular expression match object.
  LineTuple = collections.namedtuple('StackLineTuple',
                                     'address, lib_path, lib_offset, match')

  def __init__(self, android_abi, memory_map, apk_translator):
    """Initialize instance."""
    hex_addr = _HexAddressRegexpFor(android_abi)

    # pylint: disable=line-too-long
    # A regular expression used to recognize stack entries like:
    #
    #    #05  bf89a180  bf89a1e4  [stack]
    #         bf89a1c8  a0c01c51  /data/app/com.google.android.apps.chrome-2/base.apk
    #         bf89a080  00000000
    #         ........  ........
    # pylint: enable=line-too-long
    self._re_stack_line = re.compile(
        r'\s+(?P<frame_number>#[0-9]+)?\s*' +
        r'(?P<stack_addr>' + hex_addr + r')\s+' +
        r'(?P<stack_value>' + hex_addr + r')' +
        r'(\s+(?P<location>[^ \t]+))?')

    self._re_stack_abbrev = re.compile(r'\s+[.]+\s+[.]+')

    self._memory_map = memory_map
    self._apk_translator = apk_translator
    self._in_section = False

  def _ParseLine(self, line):
    """Check a given input line for a relevant _re_stack_line match.

    Args:
      line: input tombstone line.
    Returns:
      A LineTuple instance on success, None on failure.
    """
    line = line.rstrip()
    if not self._in_section:
      self._in_section = line.startswith('stack:')
      return None

    m = self._re_stack_line.match(line)
    if not m:
      if not self._re_stack_abbrev.match(line):
        self._in_section = False
      return None

    location = m.group('location')
    if not location:
      return None

    if not location.endswith('.apk') and not location.endswith('.so'):
      return None

    addr = int(m.group('stack_value'), 16)
    t = self._memory_map.FindSectionForAddress(addr)
    if t is None:
      return None

    lib_path = t.path
    lib_offset = t.offset + (addr - t.address)

    if lib_path.endswith('.apk'):
      lib_path, lib_offset = self._apk_translator.TranslatePath(
          lib_path, lib_offset)

    return self.LineTuple(addr, lib_path, lib_offset, m)

  def FindLibraryOffsets(self, input_lines, in_section=False):
    """Parse a tombstone's stack section and find all library offsets in it.

    Args:
      input_lines: List or iterables of intput tombstone lines.
      in_section: Optional. If True, considers that the stack section has
        already started.
    Returns:
      A dictionary mapping device library paths to sets of offsets within
      then.
    """
    result = collections.defaultdict(set)
    self._in_section = in_section
    for line in input_lines:
      t = self._ParseLine(line)
      if t:
        result[t.lib_path].add(t.lib_offset)
    return result

  def TranslateLine(self, line, symbol_resolver=None):
    """Try to translate a line of the stack dump."""
    t = self._ParseLine(line)
    if not t:
      return line

    symbol_info = symbol_resolver.FindSymbolInfo(t.lib_path, t.lib_offset)
    if not symbol_info:
      return line

    pos = t.match.start('location')
    pos2 = t.match.end('location')
    return '%s%s (%s)%s' % (line[:pos], t.lib_path, symbol_info, line[pos2:])