| # 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. |
| |
| from __future__ import print_function |
| |
| 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:]) |