summaryrefslogtreecommitdiff
path: root/chromium/build/fuchsia/runner_v2/symbolizer.py
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/build/fuchsia/runner_v2/symbolizer.py')
-rw-r--r--chromium/build/fuchsia/runner_v2/symbolizer.py229
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})