diff options
Diffstat (limited to 'chromium/build/fuchsia/runner_v2/symbolizer.py')
-rw-r--r-- | chromium/build/fuchsia/runner_v2/symbolizer.py | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/chromium/build/fuchsia/runner_v2/symbolizer.py b/chromium/build/fuchsia/runner_v2/symbolizer.py new file mode 100644 index 00000000000..4764c26dd62 --- /dev/null +++ b/chromium/build/fuchsia/runner_v2/symbolizer.py @@ -0,0 +1,229 @@ +# 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'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}) |