| # 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 logging |
| import os |
| import re |
| import subprocess |
| |
| # Matches the coarse syntax of a backtrace entry. |
| _BACKTRACE_PREFIX_RE = re.compile(r'(\[[0-9.]+\] )?bt#(?P<frame_id>\d+): ') |
| |
| # Matches the specific fields of a backtrace entry. |
| # Back-trace line matcher/parser assumes that 'pc' is always present, and |
| # expects that 'sp' and ('binary','pc_offset') may also be provided. |
| _BACKTRACE_ENTRY_RE = re.compile( |
| r'pc 0(?:x[0-9a-f]+)?' + |
| r'(?: sp 0x[0-9a-f]+)?' + |
| r'(?: \((?P<binary>\S+),(?P<pc_offset>0x[0-9a-f]+)\))?$') |
| |
| |
| def _GetUnstrippedPath(path): |
| """If there is a binary located at |path|, returns a path to its unstripped |
| source. |
| |
| Returns None if |path| isn't a binary or doesn't exist in the lib.unstripped |
| or exe.unstripped directories.""" |
| |
| if path.endswith('.so'): |
| maybe_unstripped_path = os.path.normpath( |
| os.path.join(path, os.path.pardir, 'lib.unstripped', |
| os.path.basename(path))) |
| else: |
| maybe_unstripped_path = os.path.normpath( |
| os.path.join(path, os.path.pardir, 'exe.unstripped', |
| os.path.basename(path))) |
| |
| if not os.path.exists(maybe_unstripped_path): |
| return None |
| |
| with open(maybe_unstripped_path, 'rb') as f: |
| file_tag = f.read(4) |
| if file_tag != '\x7fELF': |
| logging.warn('Expected an ELF binary: ' + maybe_unstripped_path) |
| return None |
| |
| return maybe_unstripped_path |
| |
| |
| def FilterStream(stream, package_name, manifest_path, output_dir): |
| """Looks for backtrace lines from an iterable |stream| and symbolizes them. |
| Yields a stream of strings with symbolized entries replaced.""" |
| |
| return _SymbolizerFilter(package_name, |
| manifest_path, |
| output_dir).SymbolizeStream(stream) |
| |
| |
| class _SymbolizerFilter(object): |
| """Adds backtrace symbolization capabilities to a process output stream.""" |
| |
| def __init__(self, package_name, manifest_path, output_dir): |
| self._symbols_mapping = {} |
| self._output_dir = output_dir |
| self._package_name = package_name |
| |
| # Compute remote/local path mappings using the manifest data. |
| for next_line in open(manifest_path): |
| target, source = next_line.strip().split('=') |
| stripped_binary_path = _GetUnstrippedPath(os.path.join(output_dir, |
| source)) |
| if not stripped_binary_path: |
| continue |
| |
| self._symbols_mapping[os.path.basename(target)] = stripped_binary_path |
| self._symbols_mapping[target] = stripped_binary_path |
| if target == 'bin/app': |
| self._symbols_mapping[package_name] = stripped_binary_path |
| logging.debug('Symbols: %s -> %s' % (source, target)) |
| |
| def _SymbolizeEntries(self, entries): |
| """Symbolizes the parsed backtrace |entries| by calling addr2line. |
| |
| Returns a set of (frame_id, result) pairs.""" |
| |
| filename_re = re.compile(r'at ([-._a-zA-Z0-9/+]+):(\d+)') |
| |
| # Use addr2line to symbolize all the |pc_offset|s in |entries| in one go. |
| # Entries with no |debug_binary| are also processed here, so that we get |
| # consistent output in that case, with the cannot-symbolize case. |
| addr2line_output = None |
| if entries[0].has_key('debug_binary'): |
| addr2line_args = (['addr2line', '-Cipf', '-p', |
| '--exe=' + entries[0]['debug_binary']] + |
| map(lambda entry: entry['pc_offset'], entries)) |
| addr2line_output = subprocess.check_output(addr2line_args).splitlines() |
| assert addr2line_output |
| |
| results = {} |
| for entry in entries: |
| raw, frame_id = entry['raw'], entry['frame_id'] |
| prefix = '#%s: ' % frame_id |
| |
| if not addr2line_output: |
| # Either there was no addr2line output, or too little of it. |
| filtered_line = raw |
| else: |
| output_line = addr2line_output.pop(0) |
| |
| # Relativize path to the current working (output) directory if we see |
| # a filename. |
| def RelativizePath(m): |
| relpath = os.path.relpath(os.path.normpath(m.group(1))) |
| return 'at ' + relpath + ':' + m.group(2) |
| filtered_line = filename_re.sub(RelativizePath, output_line) |
| |
| if '??' in filtered_line.split(): |
| # If symbolization fails just output the raw backtrace. |
| filtered_line = raw |
| else: |
| # Release builds may inline things, resulting in "(inlined by)" lines. |
| inlined_by_prefix = " (inlined by)" |
| while (addr2line_output and |
| addr2line_output[0].startswith(inlined_by_prefix)): |
| inlined_by_line = \ |
| '\n' + (' ' * len(prefix)) + addr2line_output.pop(0) |
| filtered_line += filename_re.sub(RelativizePath, inlined_by_line) |
| |
| results[entry['frame_id']] = prefix + filtered_line |
| |
| return results |
| |
| def _LookupDebugBinary(self, entry): |
| """Looks up the binary listed in |entry| in the |_symbols_mapping|. |
| Returns the corresponding host-side binary's filename, or None.""" |
| |
| binary = entry['binary'] |
| if not binary: |
| return None |
| |
| app_prefix = 'app:' |
| if binary.startswith(app_prefix): |
| binary = binary[len(app_prefix):] |
| |
| # We change directory into /system/ before running the target executable, so |
| # all paths are relative to "/system/", and will typically start with "./". |
| # Some crashes still uses the full filesystem path, so cope with that, too. |
| pkg_prefix = '/pkg/' |
| cwd_prefix = './' |
| if binary.startswith(cwd_prefix): |
| binary = binary[len(cwd_prefix):] |
| elif binary.startswith(pkg_prefix): |
| binary = binary[len(pkg_prefix):] |
| # Allow other paths to pass-through; sometimes neither prefix is present. |
| |
| if binary in self._symbols_mapping: |
| return self._symbols_mapping[binary] |
| |
| # |binary| may be truncated by the crashlogger, so if there is a unique |
| # match for the truncated name in |symbols_mapping|, use that instead. |
| matches = filter(lambda x: x.startswith(binary), |
| self._symbols_mapping.keys()) |
| if len(matches) == 1: |
| return self._symbols_mapping[matches[0]] |
| |
| return None |
| |
| def _SymbolizeBacktrace(self, backtrace): |
| """Group |backtrace| entries according to the associated binary, and locate |
| the path to the debug symbols for that binary, if any.""" |
| |
| batches = {} |
| |
| for entry in backtrace: |
| debug_binary = self._LookupDebugBinary(entry) |
| if debug_binary: |
| entry['debug_binary'] = debug_binary |
| batches.setdefault(debug_binary, []).append(entry) |
| |
| # Run _SymbolizeEntries on each batch and collate the results. |
| symbolized = {} |
| for batch in batches.itervalues(): |
| symbolized.update(self._SymbolizeEntries(batch)) |
| |
| # Map each entry to its symbolized form, by frame-id, and return the list. |
| return map(lambda entry: symbolized[entry['frame_id']], backtrace) |
| |
| def SymbolizeStream(self, stream): |
| """Creates a symbolized logging stream object using the output from |
| |stream|.""" |
| |
| # A buffer of backtrace entries awaiting symbolization, stored as dicts: |
| # raw: The original back-trace line that followed the prefix. |
| # frame_id: backtrace frame number (starting at 0). |
| # binary: path to executable code corresponding to the current frame. |
| # pc_offset: memory offset within the executable. |
| backtrace_entries = [] |
| |
| # Read from the stream until we hit EOF. |
| for line in stream: |
| line = line.rstrip() |
| |
| # Look for the back-trace prefix, otherwise just emit the line. |
| matched = _BACKTRACE_PREFIX_RE.match(line) |
| if not matched: |
| yield line |
| continue |
| backtrace_line = line[matched.end():] |
| |
| # If this was the end of a back-trace then symbolize and emit it. |
| frame_id = matched.group('frame_id') |
| if backtrace_line == 'end': |
| if backtrace_entries: |
| for processed in self._SymbolizeBacktrace(backtrace_entries): |
| yield processed |
| backtrace_entries = [] |
| continue |
| |
| # Parse the program-counter offset, etc into |backtrace_entries|. |
| matched = _BACKTRACE_ENTRY_RE.match(backtrace_line) |
| if matched: |
| # |binary| and |pc_offset| will be None if not present. |
| backtrace_entries.append( |
| {'raw': backtrace_line, 'frame_id': frame_id, |
| 'binary': matched.group('binary'), |
| 'pc_offset': matched.group('pc_offset')}) |
| else: |
| backtrace_entries.append( |
| {'raw': backtrace_line, 'frame_id': frame_id, |
| 'binary': None, 'pc_offset': None}) |