1#!/usr/bin/python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Utility to decode a crash dump generated by untrusted_crash_dump.[ch]
7
8Currently this produces a simple stack trace.
9"""
10
11import json
12import optparse
13import os
14import posixpath
15import subprocess
16import sys
17
18
19class CoreDecoder(object):
20  """Class to process core dumps."""
21
22  def __init__(self, main_nexe, nmf_filename,
23               addr2line, library_paths, platform):
24    """Construct and object to process core dumps.
25
26    Args:
27      main_nexe: nexe to resolve NaClMain references from.
28      nmf_filename: nmf to resolve references from.
29      addr2line: path to appropriate addr2line.
30      library_paths: list of paths to search for libraries.
31      platform: platform string to use in nmf files.
32    """
33    self.main_nexe = main_nexe
34    self.nmf_filename = nmf_filename
35    if nmf_filename == '-':
36      self.nmf_data = {}
37    else:
38      self.nmf_data = json.load(open(nmf_filename))
39    self.addr2line = addr2line
40    self.library_paths = library_paths
41    self.platform = platform
42
43  def _SelectModulePath(self, filename):
44    """Select which path to get a module from.
45
46    Args:
47      filename: filename of a module (as appears in phdrs).
48    Returns:
49      Full local path to the file.
50      Derived by consulting the manifest.
51    """
52    # For some names try the main nexe.
53    # NaClMain is the argv[0] setup in sel_main.c
54    # (null) shows up in chrome.
55    if self.main_nexe is not None and filename in ['NaClMain', '(null)']:
56      return self.main_nexe
57    filepart = posixpath.basename(filename)
58    nmf_entry = self.nmf_data.get('files', {}).get(filepart, {})
59    nmf_url = nmf_entry.get(self.platform, {}).get('url')
60    # Try filename directly if not in manifest.
61    if nmf_url is None:
62      return filename
63    # Look for the module relative to the manifest (if any),
64    # then in other search paths.
65    paths = []
66    if self.nmf_filename != '-':
67      paths.append(os.path.dirname(self.nmf_filename))
68    paths.extend(self.library_paths)
69    for path in paths:
70      pfilename = os.path.join(path, nmf_url)
71      if os.path.exists(pfilename):
72        return pfilename
73    # If nothing else, try the path directly.
74    return filename
75
76  def _DecodeAddressSegment(self, segments, address):
77    """Convert an address to a segment relative one, plus filename.
78
79    Args:
80      segments: a list of phdr segments.
81      address: a process wide code address.
82    Returns:
83      A tuple of filename and segment relative address.
84    """
85    for segment in segments:
86      for phdr in segment['dlpi_phdr']:
87        start = segment['dlpi_addr'] + phdr['p_vaddr']
88        end = start + phdr['p_memsz']
89        if address >= start and address < end:
90          return (segment['dlpi_name'], address - segment['dlpi_addr'])
91    return ('(null)', address)
92
93  def _Addr2Line(self, segments, address):
94    """Use addr2line to decode a code address.
95
96    Args:
97      segments: A list of phdr segments.
98      address: a code address.
99    Returns:
100      A list of dicts containing: function, filename, lineno.
101    """
102    filename, address = self._DecodeAddressSegment(segments, address)
103    filename = self._SelectModulePath(filename)
104    if not os.path.exists(filename):
105      return [{
106          'function': 'Unknown_function',
107          'filename': 'unknown_file',
108          'lineno': -1,
109      }]
110    # Use address - 1 to get the call site instead of the line after.
111    address -= 1
112    cmd = [
113        self.addr2line, '-f', '--inlines', '-e', filename, '0x%08x' % address,
114    ]
115    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
116    process_stdout, _ = process.communicate()
117    assert process.returncode == 0
118    lines = process_stdout.splitlines()
119    assert len(lines) % 2 == 0
120    results = []
121    for index in xrange(len(lines) / 2):
122      func = lines[index * 2]
123      afilename, lineno = lines[index * 2 + 1].split(':', 1)
124      results.append({
125          'function': func,
126          'filename': afilename,
127          'lineno': int(lineno),
128      })
129    return results
130
131  def Decode(self, text):
132    core = json.loads(text)
133    for frame in core['frames']:
134      frame['scopes'] = self._Addr2Line(core['segments'], frame['prog_ctr'])
135    return core
136
137
138  def LoadAndDecode(self, core_path):
139    """Given a core.json file, load and embellish with decoded addresses.
140
141    Args:
142      core_path: source file containing a dump.
143    Returns:
144      An embellished core dump dict (decoded code addresses).
145    """
146    core = json.load(open(core_path))
147    for frame in core['frames']:
148      frame['scopes'] = self._Addr2Line(core['segments'], frame['prog_ctr'])
149    return core
150
151  def StackTrace(self, info):
152    """Convert a decoded core.json dump to a simple stack trace.
153
154    Args:
155      info: core.json info with decoded code addresses.
156    Returns:
157      A list of dicts with filename, lineno, function (deepest first).
158    """
159    trace = []
160    for frame in info['frames']:
161      for scope in frame['scopes']:
162        trace.append(scope)
163    return trace
164
165  def PrintTrace(self, trace, out):
166    """Print a trace to a file like object.
167
168    Args:
169      trace: A list of [filename, lineno, function] (deepest first).
170      out: file like object to output the trace to.
171    """
172    for scope in trace:
173      out.write('%s at %s:%d\n' % (
174          scope['function'],
175          scope['filename'],
176          scope['lineno']))
177
178
179def Main(args):
180  parser = optparse.OptionParser(
181      usage='USAGE: %prog [options] <core.json>')
182  parser.add_option('-m', '--main-nexe', dest='main_nexe',
183                    help='nexe to resolve NaClMain references from')
184  parser.add_option('-n', '--nmf', dest='nmf_filename', default='-',
185                    help='nmf to resolve references from')
186  parser.add_option('-a', '--addr2line', dest='addr2line',
187                    help='path to appropriate addr2line')
188  parser.add_option('-L', '--library-path', dest='library_paths',
189                    action='append', default=[],
190                    help='path to search for shared libraries')
191  parser.add_option('-p', '--platform', dest='platform',
192                    help='platform in a style match nmf files')
193  options, args = parser.parse_args(args)
194  if len(args) != 1:
195    parser.print_help()
196    sys.exit(1)
197  decoder = CoreDecoder(
198      main_nexe=options.main_nexe,
199      nmf_filename=options.nmf_filename,
200      addr2line=options.add2line,
201      library_paths=options.library_paths,
202      platform=options.platform)
203  info = decoder.LoadAndDecode(args[0])
204  trace = decoder.StackTrace(info)
205  decoder.PrintTrace(trace, sys.stdout)
206
207
208if __name__ == '__main__':
209  Main(sys.argv[1:])
210