xref: /openbsd-src/gnu/llvm/lldb/examples/python/crashlog.py (revision 5a38ef86d0b61900239c7913d24a05e7b88a58f0)
1#!/usr/bin/env python3
2
3#----------------------------------------------------------------------
4# Be sure to add the python path that points to the LLDB shared library.
5#
6# To use this in the embedded python interpreter using "lldb":
7#
8#   cd /path/containing/crashlog.py
9#   lldb
10#   (lldb) script import crashlog
11#   "crashlog" command installed, type "crashlog --help" for detailed help
12#   (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash
13#
14# The benefit of running the crashlog command inside lldb in the
15# embedded python interpreter is when the command completes, there
16# will be a target with all of the files loaded at the locations
17# described in the crash log. Only the files that have stack frames
18# in the backtrace will be loaded unless the "--load-all" option
19# has been specified. This allows users to explore the program in the
20# state it was in right at crash time.
21#
22# On MacOSX csh, tcsh:
23#   ( setenv PYTHONPATH /path/to/LLDB.framework/Resources/Python ; ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash )
24#
25# On MacOSX sh, bash:
26#   PYTHONPATH=/path/to/LLDB.framework/Resources/Python ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash
27#----------------------------------------------------------------------
28
29from __future__ import print_function
30import cmd
31import datetime
32import glob
33import optparse
34import os
35import platform
36import plistlib
37import re
38import shlex
39import string
40import subprocess
41import sys
42import time
43import uuid
44import json
45
46try:
47    # First try for LLDB in case PYTHONPATH is already correctly setup.
48    import lldb
49except ImportError:
50    # Ask the command line driver for the path to the lldb module. Copy over
51    # the environment so that SDKROOT is propagated to xcrun.
52    env = os.environ.copy()
53    env['LLDB_DEFAULT_PYTHON_VERSION'] = str(sys.version_info.major)
54    command =  ['xcrun', 'lldb', '-P'] if platform.system() == 'Darwin' else ['lldb', '-P']
55    # Extend the PYTHONPATH if the path exists and isn't already there.
56    lldb_python_path = subprocess.check_output(command, env=env).decode("utf-8").strip()
57    if os.path.exists(lldb_python_path) and not sys.path.__contains__(lldb_python_path):
58        sys.path.append(lldb_python_path)
59    # Try importing LLDB again.
60    try:
61        import lldb
62    except ImportError:
63        print("error: couldn't locate the 'lldb' module, please set PYTHONPATH correctly")
64        sys.exit(1)
65
66from lldb.utils import symbolication
67
68
69def read_plist(s):
70    if sys.version_info.major == 3:
71        return plistlib.loads(s)
72    else:
73        return plistlib.readPlistFromString(s)
74
75class CrashLog(symbolication.Symbolicator):
76    class Thread:
77        """Class that represents a thread in a darwin crash log"""
78
79        def __init__(self, index, app_specific_backtrace):
80            self.index = index
81            self.frames = list()
82            self.idents = list()
83            self.registers = dict()
84            self.reason = None
85            self.queue = None
86            self.app_specific_backtrace = app_specific_backtrace
87
88        def dump(self, prefix):
89            if self.app_specific_backtrace:
90                print("%Application Specific Backtrace[%u] %s" % (prefix, self.index, self.reason))
91            else:
92                print("%sThread[%u] %s" % (prefix, self.index, self.reason))
93            if self.frames:
94                print("%s  Frames:" % (prefix))
95                for frame in self.frames:
96                    frame.dump(prefix + '    ')
97            if self.registers:
98                print("%s  Registers:" % (prefix))
99                for reg in self.registers.keys():
100                    print("%s    %-8s = %#16.16x" % (prefix, reg, self.registers[reg]))
101
102        def dump_symbolicated(self, crash_log, options):
103            this_thread_crashed = self.app_specific_backtrace
104            if not this_thread_crashed:
105                this_thread_crashed = self.did_crash()
106                if options.crashed_only and this_thread_crashed == False:
107                    return
108
109            print("%s" % self)
110            display_frame_idx = -1
111            for frame_idx, frame in enumerate(self.frames):
112                disassemble = (
113                    this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth
114                if frame_idx == 0:
115                    symbolicated_frame_addresses = crash_log.symbolicate(
116                        frame.pc & crash_log.addr_mask, options.verbose)
117                else:
118                    # Any frame above frame zero and we have to subtract one to
119                    # get the previous line entry
120                    symbolicated_frame_addresses = crash_log.symbolicate(
121                        (frame.pc & crash_log.addr_mask) - 1, options.verbose)
122
123                if symbolicated_frame_addresses:
124                    symbolicated_frame_address_idx = 0
125                    for symbolicated_frame_address in symbolicated_frame_addresses:
126                        display_frame_idx += 1
127                        print('[%3u] %s' % (frame_idx, symbolicated_frame_address))
128                        if (options.source_all or self.did_crash(
129                        )) and display_frame_idx < options.source_frames and options.source_context:
130                            source_context = options.source_context
131                            line_entry = symbolicated_frame_address.get_symbol_context().line_entry
132                            if line_entry.IsValid():
133                                strm = lldb.SBStream()
134                                if line_entry:
135                                    crash_log.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers(
136                                        line_entry.file, line_entry.line, source_context, source_context, "->", strm)
137                                source_text = strm.GetData()
138                                if source_text:
139                                    # Indent the source a bit
140                                    indent_str = '    '
141                                    join_str = '\n' + indent_str
142                                    print('%s%s' % (indent_str, join_str.join(source_text.split('\n'))))
143                        if symbolicated_frame_address_idx == 0:
144                            if disassemble:
145                                instructions = symbolicated_frame_address.get_instructions()
146                                if instructions:
147                                    print()
148                                    symbolication.disassemble_instructions(
149                                        crash_log.get_target(),
150                                        instructions,
151                                        frame.pc,
152                                        options.disassemble_before,
153                                        options.disassemble_after,
154                                        frame.index > 0)
155                                    print()
156                        symbolicated_frame_address_idx += 1
157                else:
158                    print(frame)
159            if self.registers:
160                print()
161                for reg in self.registers.keys():
162                    print("    %-8s = %#16.16x" % (reg, self.registers[reg]))
163
164        def add_ident(self, ident):
165            if ident not in self.idents:
166                self.idents.append(ident)
167
168        def did_crash(self):
169            return self.reason is not None
170
171        def __str__(self):
172            if self.app_specific_backtrace:
173                s = "Application Specific Backtrace[%u]" % self.index
174            else:
175                s = "Thread[%u]" % self.index
176            if self.reason:
177                s += ' %s' % self.reason
178            return s
179
180    class Frame:
181        """Class that represents a stack frame in a thread in a darwin crash log"""
182
183        def __init__(self, index, pc, description):
184            self.pc = pc
185            self.description = description
186            self.index = index
187
188        def __str__(self):
189            if self.description:
190                return "[%3u] 0x%16.16x %s" % (
191                    self.index, self.pc, self.description)
192            else:
193                return "[%3u] 0x%16.16x" % (self.index, self.pc)
194
195        def dump(self, prefix):
196            print("%s%s" % (prefix, str(self)))
197
198    class DarwinImage(symbolication.Image):
199        """Class that represents a binary images in a darwin crash log"""
200        dsymForUUIDBinary = '/usr/local/bin/dsymForUUID'
201        if not os.path.exists(dsymForUUIDBinary):
202            try:
203                dsymForUUIDBinary = subprocess.check_output('which dsymForUUID',
204                                                            shell=True).decode("utf-8").rstrip('\n')
205            except:
206                dsymForUUIDBinary = ""
207
208        dwarfdump_uuid_regex = re.compile(
209            'UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
210
211        def __init__(
212                self,
213                text_addr_lo,
214                text_addr_hi,
215                identifier,
216                version,
217                uuid,
218                path,
219                verbose):
220            symbolication.Image.__init__(self, path, uuid)
221            self.add_section(
222                symbolication.Section(
223                    text_addr_lo,
224                    text_addr_hi,
225                    "__TEXT"))
226            self.identifier = identifier
227            self.version = version
228            self.verbose = verbose
229
230        def show_symbol_progress(self):
231            """
232            Hide progress output and errors from system frameworks as they are plentiful.
233            """
234            if self.verbose:
235                return True
236            return not (self.path.startswith("/System/Library/") or
237                        self.path.startswith("/usr/lib/"))
238
239
240        def find_matching_slice(self):
241            dwarfdump_cmd_output = subprocess.check_output(
242                'dwarfdump --uuid "%s"' % self.path, shell=True).decode("utf-8")
243            self_uuid = self.get_uuid()
244            for line in dwarfdump_cmd_output.splitlines():
245                match = self.dwarfdump_uuid_regex.search(line)
246                if match:
247                    dwarf_uuid_str = match.group(1)
248                    dwarf_uuid = uuid.UUID(dwarf_uuid_str)
249                    if self_uuid == dwarf_uuid:
250                        self.resolved_path = self.path
251                        self.arch = match.group(2)
252                        return True
253            if not self.resolved_path:
254                self.unavailable = True
255                if self.show_symbol_progress():
256                    print(("error\n    error: unable to locate '%s' with UUID %s"
257                           % (self.path, self.get_normalized_uuid_string())))
258                return False
259
260        def locate_module_and_debug_symbols(self):
261            # Don't load a module twice...
262            if self.resolved:
263                return True
264            # Mark this as resolved so we don't keep trying
265            self.resolved = True
266            uuid_str = self.get_normalized_uuid_string()
267            if self.show_symbol_progress():
268                print('Getting symbols for %s %s...' % (uuid_str, self.path), end=' ')
269            if os.path.exists(self.dsymForUUIDBinary):
270                dsym_for_uuid_command = '%s %s' % (
271                    self.dsymForUUIDBinary, uuid_str)
272                s = subprocess.check_output(dsym_for_uuid_command, shell=True)
273                if s:
274                    try:
275                        plist_root = read_plist(s)
276                    except:
277                        print(("Got exception: ", sys.exc_info()[1], " handling dsymForUUID output: \n", s))
278                        raise
279                    if plist_root:
280                        plist = plist_root[uuid_str]
281                        if plist:
282                            if 'DBGArchitecture' in plist:
283                                self.arch = plist['DBGArchitecture']
284                            if 'DBGDSYMPath' in plist:
285                                self.symfile = os.path.realpath(
286                                    plist['DBGDSYMPath'])
287                            if 'DBGSymbolRichExecutable' in plist:
288                                self.path = os.path.expanduser(
289                                    plist['DBGSymbolRichExecutable'])
290                                self.resolved_path = self.path
291            if not self.resolved_path and os.path.exists(self.path):
292                if not self.find_matching_slice():
293                    return False
294            if not self.resolved_path and not os.path.exists(self.path):
295                try:
296                    dsym = subprocess.check_output(
297                        ["/usr/bin/mdfind",
298                         "com_apple_xcode_dsym_uuids == %s"%uuid_str]).decode("utf-8")[:-1]
299                    if dsym and os.path.exists(dsym):
300                        print(('falling back to binary inside "%s"'%dsym))
301                        self.symfile = dsym
302                        dwarf_dir = os.path.join(dsym, 'Contents/Resources/DWARF')
303                        for filename in os.listdir(dwarf_dir):
304                            self.path = os.path.join(dwarf_dir, filename)
305                            if not self.find_matching_slice():
306                                return False
307                            break
308                except:
309                    pass
310            if (self.resolved_path and os.path.exists(self.resolved_path)) or (
311                    self.path and os.path.exists(self.path)):
312                print('ok')
313                return True
314            else:
315                self.unavailable = True
316            return False
317
318    def __init__(self, debugger, path, verbose):
319        """CrashLog constructor that take a path to a darwin crash log file"""
320        symbolication.Symbolicator.__init__(self, debugger)
321        self.path = os.path.expanduser(path)
322        self.info_lines = list()
323        self.system_profile = list()
324        self.threads = list()
325        self.backtraces = list()  # For application specific backtraces
326        self.idents = list()  # A list of the required identifiers for doing all stack backtraces
327        self.crashed_thread_idx = -1
328        self.version = -1
329        self.target = None
330        self.verbose = verbose
331
332    def dump(self):
333        print("Crash Log File: %s" % (self.path))
334        if self.backtraces:
335            print("\nApplication Specific Backtraces:")
336            for thread in self.backtraces:
337                thread.dump('  ')
338        print("\nThreads:")
339        for thread in self.threads:
340            thread.dump('  ')
341        print("\nImages:")
342        for image in self.images:
343            image.dump('  ')
344
345    def find_image_with_identifier(self, identifier):
346        for image in self.images:
347            if image.identifier == identifier:
348                return image
349        regex_text = '^.*\.%s$' % (re.escape(identifier))
350        regex = re.compile(regex_text)
351        for image in self.images:
352            if regex.match(image.identifier):
353                return image
354        return None
355
356    def create_target(self):
357        if self.target is None:
358            self.target = symbolication.Symbolicator.create_target(self)
359            if self.target:
360                return self.target
361            # We weren't able to open the main executable as, but we can still
362            # symbolicate
363            print('crashlog.create_target()...2')
364            if self.idents:
365                for ident in self.idents:
366                    image = self.find_image_with_identifier(ident)
367                    if image:
368                        self.target = image.create_target(self.debugger)
369                        if self.target:
370                            return self.target  # success
371            print('crashlog.create_target()...3')
372            for image in self.images:
373                self.target = image.create_target(self.debugger)
374                if self.target:
375                    return self.target  # success
376            print('crashlog.create_target()...4')
377            print('error: Unable to locate any executables from the crash log.')
378            print('       Try loading the executable into lldb before running crashlog')
379            print('       and/or make sure the .dSYM bundles can be found by Spotlight.')
380        return self.target
381
382    def get_target(self):
383        return self.target
384
385
386class CrashLogFormatException(Exception):
387    pass
388
389
390class CrashLogParseException(Exception):
391   pass
392
393
394class CrashLogParser:
395    def parse(self, debugger, path, verbose):
396        try:
397            return JSONCrashLogParser(debugger, path, verbose).parse()
398        except CrashLogFormatException:
399            return TextCrashLogParser(debugger, path, verbose).parse()
400
401
402class JSONCrashLogParser:
403    def __init__(self, debugger, path, verbose):
404        self.path = os.path.expanduser(path)
405        self.verbose = verbose
406        self.crashlog = CrashLog(debugger, self.path, self.verbose)
407
408    def parse(self):
409        with open(self.path, 'r') as f:
410            buffer = f.read()
411
412        # First line is meta-data.
413        buffer = buffer[buffer.index('\n') + 1:]
414
415        try:
416            self.data = json.loads(buffer)
417        except ValueError:
418            raise CrashLogFormatException()
419
420        try:
421            self.parse_process_info(self.data)
422            self.parse_images(self.data['usedImages'])
423            self.parse_threads(self.data['threads'])
424            thread = self.crashlog.threads[self.crashlog.crashed_thread_idx]
425            reason = self.parse_crash_reason(self.data['exception'])
426            if thread.reason:
427                thread.reason = '{} {}'.format(thread.reason, reason)
428            else:
429                thread.reason = reason
430        except (KeyError, ValueError, TypeError) as e:
431            raise CrashLogParseException(
432                'Failed to parse JSON crashlog: {}: {}'.format(
433                    type(e).__name__, e))
434
435        return self.crashlog
436
437    def get_used_image(self, idx):
438        return self.data['usedImages'][idx]
439
440    def parse_process_info(self, json_data):
441        self.crashlog.process_id = json_data['pid']
442        self.crashlog.process_identifier = json_data['procName']
443        self.crashlog.process_path = json_data['procPath']
444
445    def parse_crash_reason(self, json_exception):
446        exception_type = json_exception['type']
447        exception_signal = json_exception['signal']
448        if 'codes' in json_exception:
449            exception_extra = " ({})".format(json_exception['codes'])
450        elif 'subtype' in json_exception:
451            exception_extra = " ({})".format(json_exception['subtype'])
452        else:
453            exception_extra = ""
454        return "{} ({}){}".format(exception_type, exception_signal,
455                                  exception_extra)
456
457    def parse_images(self, json_images):
458        idx = 0
459        for json_image in json_images:
460            img_uuid = uuid.UUID(json_image['uuid'])
461            low = int(json_image['base'])
462            high = int(0)
463            name = json_image['name'] if 'name' in json_image else ''
464            path = json_image['path'] if 'path' in json_image else ''
465            version = ''
466            darwin_image = self.crashlog.DarwinImage(low, high, name, version,
467                                                     img_uuid, path,
468                                                     self.verbose)
469            self.crashlog.images.append(darwin_image)
470            idx += 1
471
472    def parse_frames(self, thread, json_frames):
473        idx = 0
474        for json_frame in json_frames:
475            image_id = int(json_frame['imageIndex'])
476            ident = self.get_used_image(image_id)['name']
477            thread.add_ident(ident)
478            if ident not in self.crashlog.idents:
479                self.crashlog.idents.append(ident)
480
481            frame_offset = int(json_frame['imageOffset'])
482            image_addr = self.get_used_image(image_id)['base']
483            pc = image_addr + frame_offset
484            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
485            idx += 1
486
487    def parse_threads(self, json_threads):
488        idx = 0
489        for json_thread in json_threads:
490            thread = self.crashlog.Thread(idx, False)
491            if 'name' in json_thread:
492                thread.reason = json_thread['name']
493            if json_thread.get('triggered', False):
494                self.crashlog.crashed_thread_idx = idx
495                thread.registers = self.parse_thread_registers(
496                    json_thread['threadState'])
497            thread.queue = json_thread.get('queue')
498            self.parse_frames(thread, json_thread.get('frames', []))
499            self.crashlog.threads.append(thread)
500            idx += 1
501
502    def parse_thread_registers(self, json_thread_state):
503        registers = dict()
504        for key, state in json_thread_state.items():
505            try:
506               value = int(state['value'])
507               registers[key] = value
508            except (TypeError, ValueError):
509               pass
510        return registers
511
512
513class CrashLogParseMode:
514    NORMAL = 0
515    THREAD = 1
516    IMAGES = 2
517    THREGS = 3
518    SYSTEM = 4
519    INSTRS = 5
520
521
522class TextCrashLogParser:
523    parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]')
524    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
525    thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream')
526    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
527    app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)')
528    version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+'
529    frame_regex = re.compile(r'^([0-9]+)' r'\s'                # id
530                             r'+(.+?)'    r'\s+'               # img_name
531                             r'(' +version+ r')?'              # img_version
532                             r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr
533                             r' +(.*)'                         # offs
534                            )
535    null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)')
536    image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)'            # img_lo
537                                  r'\s+' '-' r'\s+'              #   -
538                                  r'(0x[0-9a-fA-F]+)'     r'\s+' # img_hi
539                                  r'[+]?(.+?)'            r'\s+' # img_name
540                                  r'(' +version+ ')?'            # img_version
541                                  r'(<([-0-9a-fA-F]+)>\s+)?'     # img_uuid
542                                  r'(/.*)'                       # img_path
543                                 )
544
545
546    def __init__(self, debugger, path, verbose):
547        self.path = os.path.expanduser(path)
548        self.verbose = verbose
549        self.thread = None
550        self.app_specific_backtrace = False
551        self.crashlog = CrashLog(debugger, self.path, self.verbose)
552        self.parse_mode = CrashLogParseMode.NORMAL
553        self.parsers = {
554            CrashLogParseMode.NORMAL : self.parse_normal,
555            CrashLogParseMode.THREAD : self.parse_thread,
556            CrashLogParseMode.IMAGES : self.parse_images,
557            CrashLogParseMode.THREGS : self.parse_thread_registers,
558            CrashLogParseMode.SYSTEM : self.parse_system,
559            CrashLogParseMode.INSTRS : self.parse_instructions,
560        }
561
562    def parse(self):
563        with open(self.path,'r') as f:
564            lines = f.read().splitlines()
565
566        for line in lines:
567            line_len = len(line)
568            if line_len == 0:
569                if self.thread:
570                    if self.parse_mode == CrashLogParseMode.THREAD:
571                        if self.thread.index == self.crashlog.crashed_thread_idx:
572                            self.thread.reason = ''
573                            if self.crashlog.thread_exception:
574                                self.thread.reason += self.crashlog.thread_exception
575                            if self.crashlog.thread_exception_data:
576                                self.thread.reason += " (%s)" % self.crashlog.thread_exception_data
577                        if self.app_specific_backtrace:
578                            self.crashlog.backtraces.append(self.thread)
579                        else:
580                            self.crashlog.threads.append(self.thread)
581                    self.thread = None
582                else:
583                    # only append an extra empty line if the previous line
584                    # in the info_lines wasn't empty
585                    if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]):
586                        self.crashlog.info_lines.append(line)
587                self.parse_mode = CrashLogParseMode.NORMAL
588            else:
589                self.parsers[self.parse_mode](line)
590
591        return self.crashlog
592
593
594    def parse_normal(self, line):
595        if line.startswith('Process:'):
596            (self.crashlog.process_name, pid_with_brackets) = line[
597                8:].strip().split(' [')
598            self.crashlog.process_id = pid_with_brackets.strip('[]')
599        elif line.startswith('Path:'):
600            self.crashlog.process_path = line[5:].strip()
601        elif line.startswith('Identifier:'):
602            self.crashlog.process_identifier = line[11:].strip()
603        elif line.startswith('Version:'):
604            version_string = line[8:].strip()
605            matched_pair = re.search("(.+)\((.+)\)", version_string)
606            if matched_pair:
607                self.crashlog.process_version = matched_pair.group(1)
608                self.crashlog.process_compatability_version = matched_pair.group(
609                    2)
610            else:
611                self.crashlog.process = version_string
612                self.crashlog.process_compatability_version = version_string
613        elif self.parent_process_regex.search(line):
614            parent_process_match = self.parent_process_regex.search(
615                line)
616            self.crashlog.parent_process_name = parent_process_match.group(1)
617            self.crashlog.parent_process_id = parent_process_match.group(2)
618        elif line.startswith('Exception Type:'):
619            self.crashlog.thread_exception = line[15:].strip()
620            return
621        elif line.startswith('Exception Codes:'):
622            self.crashlog.thread_exception_data = line[16:].strip()
623            return
624        elif line.startswith('Exception Subtype:'): # iOS
625            self.crashlog.thread_exception_data = line[18:].strip()
626            return
627        elif line.startswith('Crashed Thread:'):
628            self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0])
629            return
630        elif line.startswith('Triggered by Thread:'): # iOS
631            self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0])
632            return
633        elif line.startswith('Report Version:'):
634            self.crashlog.version = int(line[15:].strip())
635            return
636        elif line.startswith('System Profile:'):
637            self.parse_mode = CrashLogParseMode.SYSTEM
638            return
639        elif (line.startswith('Interval Since Last Report:') or
640                line.startswith('Crashes Since Last Report:') or
641                line.startswith('Per-App Interval Since Last Report:') or
642                line.startswith('Per-App Crashes Since Last Report:') or
643                line.startswith('Sleep/Wake UUID:') or
644                line.startswith('Anonymous UUID:')):
645            # ignore these
646            return
647        elif line.startswith('Thread'):
648            thread_state_match = self.thread_state_regex.search(line)
649            if thread_state_match:
650                self.app_specific_backtrace = False
651                thread_state_match = self.thread_regex.search(line)
652                thread_idx = int(thread_state_match.group(1))
653                self.parse_mode = CrashLogParseMode.THREGS
654                self.thread = self.crashlog.threads[thread_idx]
655                return
656            thread_insts_match  = self.thread_instrs_regex.search(line)
657            if thread_insts_match:
658                self.parse_mode = CrashLogParseMode.INSTRS
659                return
660            thread_match = self.thread_regex.search(line)
661            if thread_match:
662                self.app_specific_backtrace = False
663                self.parse_mode = CrashLogParseMode.THREAD
664                thread_idx = int(thread_match.group(1))
665                self.thread = self.crashlog.Thread(thread_idx, False)
666                return
667            return
668        elif line.startswith('Binary Images:'):
669            self.parse_mode = CrashLogParseMode.IMAGES
670            return
671        elif line.startswith('Application Specific Backtrace'):
672            app_backtrace_match = self.app_backtrace_regex.search(line)
673            if app_backtrace_match:
674                self.parse_mode = CrashLogParseMode.THREAD
675                self.app_specific_backtrace = True
676                idx = int(app_backtrace_match.group(1))
677                self.thread = self.crashlog.Thread(idx, True)
678        elif line.startswith('Last Exception Backtrace:'): # iOS
679            self.parse_mode = CrashLogParseMode.THREAD
680            self.app_specific_backtrace = True
681            idx = 1
682            self.thread = self.crashlog.Thread(idx, True)
683        self.crashlog.info_lines.append(line.strip())
684
685    def parse_thread(self, line):
686        if line.startswith('Thread'):
687            return
688        if self.null_frame_regex.search(line):
689            print('warning: thread parser ignored null-frame: "%s"' % line)
690            return
691        frame_match = self.frame_regex.search(line)
692        if frame_match:
693            (frame_id, frame_img_name, _, frame_img_version, _,
694                frame_addr, frame_ofs) = frame_match.groups()
695            ident = frame_img_name
696            self.thread.add_ident(ident)
697            if ident not in self.crashlog.idents:
698                self.crashlog.idents.append(ident)
699            self.thread.frames.append(self.crashlog.Frame(int(frame_id), int(
700                frame_addr, 0), frame_ofs))
701        else:
702            print('error: frame regex failed for line: "%s"' % line)
703
704    def parse_images(self, line):
705        image_match = self.image_regex_uuid.search(line)
706        if image_match:
707            (img_lo, img_hi, img_name, _, img_version, _,
708                _, img_uuid, img_path) = image_match.groups()
709            image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0),
710                                            img_name.strip(),
711                                            img_version.strip()
712                                            if img_version else "",
713                                            uuid.UUID(img_uuid), img_path,
714                                            self.verbose)
715            self.crashlog.images.append(image)
716        else:
717            print("error: image regex failed for: %s" % line)
718
719
720    def parse_thread_registers(self, line):
721        stripped_line = line.strip()
722        # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
723        reg_values = re.findall(
724            '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line)
725        for reg_value in reg_values:
726            (reg, value) = reg_value.split(': ')
727            self.thread.registers[reg.strip()] = int(value, 0)
728
729    def parse_system(self, line):
730        self.crashlog.system_profile.append(line)
731
732    def parse_instructions(self, line):
733        pass
734
735
736def usage():
737    print("Usage: lldb-symbolicate.py [-n name] executable-image")
738    sys.exit(0)
739
740
741class Interactive(cmd.Cmd):
742    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
743    image_option_parser = None
744
745    def __init__(self, crash_logs):
746        cmd.Cmd.__init__(self)
747        self.use_rawinput = False
748        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
749        self.crash_logs = crash_logs
750        self.prompt = '% '
751
752    def default(self, line):
753        '''Catch all for unknown command, which will exit the interpreter.'''
754        print("uknown command: %s" % line)
755        return True
756
757    def do_q(self, line):
758        '''Quit command'''
759        return True
760
761    def do_quit(self, line):
762        '''Quit command'''
763        return True
764
765    def do_symbolicate(self, line):
766        description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information,
767        inlined stack frames back to the concrete functions, and disassemble the location of the crash
768        for the first frame of the crashed thread.'''
769        option_parser = CreateSymbolicateCrashLogOptions(
770            'symbolicate', description, False)
771        command_args = shlex.split(line)
772        try:
773            (options, args) = option_parser.parse_args(command_args)
774        except:
775            return
776
777        if args:
778            # We have arguments, they must valid be crash log file indexes
779            for idx_str in args:
780                idx = int(idx_str)
781                if idx < len(self.crash_logs):
782                    SymbolicateCrashLog(self.crash_logs[idx], options)
783                else:
784                    print('error: crash log index %u is out of range' % (idx))
785        else:
786            # No arguments, symbolicate all crash logs using the options
787            # provided
788            for idx in range(len(self.crash_logs)):
789                SymbolicateCrashLog(self.crash_logs[idx], options)
790
791    def do_list(self, line=None):
792        '''Dump a list of all crash logs that are currently loaded.
793
794        USAGE: list'''
795        print('%u crash logs are loaded:' % len(self.crash_logs))
796        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
797            print('[%u] = %s' % (crash_log_idx, crash_log.path))
798
799    def do_image(self, line):
800        '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
801        usage = "usage: %prog [options] <PATH> [PATH ...]"
802        description = '''Dump information about one or more images in all crash logs. The <PATH> can be a full path, image basename, or partial path. Searches are done in this order.'''
803        command_args = shlex.split(line)
804        if not self.image_option_parser:
805            self.image_option_parser = optparse.OptionParser(
806                description=description, prog='image', usage=usage)
807            self.image_option_parser.add_option(
808                '-a',
809                '--all',
810                action='store_true',
811                help='show all images',
812                default=False)
813        try:
814            (options, args) = self.image_option_parser.parse_args(command_args)
815        except:
816            return
817
818        if args:
819            for image_path in args:
820                fullpath_search = image_path[0] == '/'
821                for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
822                    matches_found = 0
823                    for (image_idx, image) in enumerate(crash_log.images):
824                        if fullpath_search:
825                            if image.get_resolved_path() == image_path:
826                                matches_found += 1
827                                print('[%u] ' % (crash_log_idx), image)
828                        else:
829                            image_basename = image.get_resolved_path_basename()
830                            if image_basename == image_path:
831                                matches_found += 1
832                                print('[%u] ' % (crash_log_idx), image)
833                    if matches_found == 0:
834                        for (image_idx, image) in enumerate(crash_log.images):
835                            resolved_image_path = image.get_resolved_path()
836                            if resolved_image_path and string.find(
837                                    image.get_resolved_path(), image_path) >= 0:
838                                print('[%u] ' % (crash_log_idx), image)
839        else:
840            for crash_log in self.crash_logs:
841                for (image_idx, image) in enumerate(crash_log.images):
842                    print('[%u] %s' % (image_idx, image))
843        return False
844
845
846def interactive_crashlogs(debugger, options, args):
847    crash_log_files = list()
848    for arg in args:
849        for resolved_path in glob.glob(arg):
850            crash_log_files.append(resolved_path)
851
852    crash_logs = list()
853    for crash_log_file in crash_log_files:
854        try:
855            crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
856        except Exception as e:
857            print(e)
858            continue
859        if options.debug:
860            crash_log.dump()
861        if not crash_log.images:
862            print('error: no images in crash log "%s"' % (crash_log))
863            continue
864        else:
865            crash_logs.append(crash_log)
866
867    interpreter = Interactive(crash_logs)
868    # List all crash logs that were imported
869    interpreter.do_list()
870    interpreter.cmdloop()
871
872
873def save_crashlog(debugger, command, exe_ctx, result, dict):
874    usage = "usage: %prog [options] <output-path>"
875    description = '''Export the state of current target into a crashlog file'''
876    parser = optparse.OptionParser(
877        description=description,
878        prog='save_crashlog',
879        usage=usage)
880    parser.add_option(
881        '-v',
882        '--verbose',
883        action='store_true',
884        dest='verbose',
885        help='display verbose debug info',
886        default=False)
887    try:
888        (options, args) = parser.parse_args(shlex.split(command))
889    except:
890        result.PutCString("error: invalid options")
891        return
892    if len(args) != 1:
893        result.PutCString(
894            "error: invalid arguments, a single output file is the only valid argument")
895        return
896    out_file = open(args[0], 'w')
897    if not out_file:
898        result.PutCString(
899            "error: failed to open file '%s' for writing...",
900            args[0])
901        return
902    target = exe_ctx.target
903    if target:
904        identifier = target.executable.basename
905        process = exe_ctx.process
906        if process:
907            pid = process.id
908            if pid != lldb.LLDB_INVALID_PROCESS_ID:
909                out_file.write(
910                    'Process:         %s [%u]\n' %
911                    (identifier, pid))
912        out_file.write('Path:            %s\n' % (target.executable.fullpath))
913        out_file.write('Identifier:      %s\n' % (identifier))
914        out_file.write('\nDate/Time:       %s\n' %
915                       (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
916        out_file.write(
917            'OS Version:      Mac OS X %s (%s)\n' %
918            (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8")))
919        out_file.write('Report Version:  9\n')
920        for thread_idx in range(process.num_threads):
921            thread = process.thread[thread_idx]
922            out_file.write('\nThread %u:\n' % (thread_idx))
923            for (frame_idx, frame) in enumerate(thread.frames):
924                frame_pc = frame.pc
925                frame_offset = 0
926                if frame.function:
927                    block = frame.GetFrameBlock()
928                    block_range = block.range[frame.addr]
929                    if block_range:
930                        block_start_addr = block_range[0]
931                        frame_offset = frame_pc - block_start_addr.GetLoadAddress(target)
932                    else:
933                        frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target)
934                elif frame.symbol:
935                    frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target)
936                out_file.write(
937                    '%-3u %-32s 0x%16.16x %s' %
938                    (frame_idx, frame.module.file.basename, frame_pc, frame.name))
939                if frame_offset > 0:
940                    out_file.write(' + %u' % (frame_offset))
941                line_entry = frame.line_entry
942                if line_entry:
943                    if options.verbose:
944                        # This will output the fullpath + line + column
945                        out_file.write(' %s' % (line_entry))
946                    else:
947                        out_file.write(
948                            ' %s:%u' %
949                            (line_entry.file.basename, line_entry.line))
950                        column = line_entry.column
951                        if column:
952                            out_file.write(':%u' % (column))
953                out_file.write('\n')
954
955        out_file.write('\nBinary Images:\n')
956        for module in target.modules:
957            text_segment = module.section['__TEXT']
958            if text_segment:
959                text_segment_load_addr = text_segment.GetLoadAddress(target)
960                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
961                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
962                    identifier = module.file.basename
963                    module_version = '???'
964                    module_version_array = module.GetVersion()
965                    if module_version_array:
966                        module_version = '.'.join(
967                            map(str, module_version_array))
968                    out_file.write(
969                        '    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' %
970                        (text_segment_load_addr,
971                         text_segment_end_load_addr,
972                         identifier,
973                         module_version,
974                         module.GetUUIDString(),
975                         module.file.fullpath))
976        out_file.close()
977    else:
978        result.PutCString("error: invalid target")
979
980
981def Symbolicate(debugger, command, result, dict):
982    try:
983        SymbolicateCrashLogs(debugger, shlex.split(command))
984    except Exception as e:
985        result.PutCString("error: python exception: %s" % e)
986
987
988def SymbolicateCrashLog(crash_log, options):
989    if options.debug:
990        crash_log.dump()
991    if not crash_log.images:
992        print('error: no images in crash log')
993        return
994
995    if options.dump_image_list:
996        print("Binary Images:")
997        for image in crash_log.images:
998            if options.verbose:
999                print(image.debug_dump())
1000            else:
1001                print(image)
1002
1003    target = crash_log.create_target()
1004    if not target:
1005        return
1006    exe_module = target.GetModuleAtIndex(0)
1007    images_to_load = list()
1008    loaded_images = list()
1009    if options.load_all_images:
1010        # --load-all option was specified, load everything up
1011        for image in crash_log.images:
1012            images_to_load.append(image)
1013    else:
1014        # Only load the images found in stack frames for the crashed threads
1015        if options.crashed_only:
1016            for thread in crash_log.threads:
1017                if thread.did_crash():
1018                    for ident in thread.idents:
1019                        images = crash_log.find_images_with_identifier(ident)
1020                        if images:
1021                            for image in images:
1022                                images_to_load.append(image)
1023                        else:
1024                            print('error: can\'t find image for identifier "%s"' % ident)
1025        else:
1026            for ident in crash_log.idents:
1027                images = crash_log.find_images_with_identifier(ident)
1028                if images:
1029                    for image in images:
1030                        images_to_load.append(image)
1031                else:
1032                    print('error: can\'t find image for identifier "%s"' % ident)
1033
1034    for image in images_to_load:
1035        if image not in loaded_images:
1036            err = image.add_module(target)
1037            if err:
1038                print(err)
1039            else:
1040                loaded_images.append(image)
1041
1042    if crash_log.backtraces:
1043        for thread in crash_log.backtraces:
1044            thread.dump_symbolicated(crash_log, options)
1045            print()
1046
1047    for thread in crash_log.threads:
1048        thread.dump_symbolicated(crash_log, options)
1049        print()
1050
1051
1052def CreateSymbolicateCrashLogOptions(
1053        command_name,
1054        description,
1055        add_interactive_options):
1056    usage = "usage: %prog [options] <FILE> [FILE ...]"
1057    option_parser = optparse.OptionParser(
1058        description=description, prog='crashlog', usage=usage)
1059    option_parser.add_option(
1060        '--verbose',
1061        '-v',
1062        action='store_true',
1063        dest='verbose',
1064        help='display verbose debug info',
1065        default=False)
1066    option_parser.add_option(
1067        '--debug',
1068        '-g',
1069        action='store_true',
1070        dest='debug',
1071        help='display verbose debug logging',
1072        default=False)
1073    option_parser.add_option(
1074        '--load-all',
1075        '-a',
1076        action='store_true',
1077        dest='load_all_images',
1078        help='load all executable images, not just the images found in the crashed stack frames',
1079        default=False)
1080    option_parser.add_option(
1081        '--images',
1082        action='store_true',
1083        dest='dump_image_list',
1084        help='show image list',
1085        default=False)
1086    option_parser.add_option(
1087        '--debug-delay',
1088        type='int',
1089        dest='debug_delay',
1090        metavar='NSEC',
1091        help='pause for NSEC seconds for debugger',
1092        default=0)
1093    option_parser.add_option(
1094        '--crashed-only',
1095        '-c',
1096        action='store_true',
1097        dest='crashed_only',
1098        help='only symbolicate the crashed thread',
1099        default=False)
1100    option_parser.add_option(
1101        '--disasm-depth',
1102        '-d',
1103        type='int',
1104        dest='disassemble_depth',
1105        help='set the depth in stack frames that should be disassembled (default is 1)',
1106        default=1)
1107    option_parser.add_option(
1108        '--disasm-all',
1109        '-D',
1110        action='store_true',
1111        dest='disassemble_all_threads',
1112        help='enabled disassembly of frames on all threads (not just the crashed thread)',
1113        default=False)
1114    option_parser.add_option(
1115        '--disasm-before',
1116        '-B',
1117        type='int',
1118        dest='disassemble_before',
1119        help='the number of instructions to disassemble before the frame PC',
1120        default=4)
1121    option_parser.add_option(
1122        '--disasm-after',
1123        '-A',
1124        type='int',
1125        dest='disassemble_after',
1126        help='the number of instructions to disassemble after the frame PC',
1127        default=4)
1128    option_parser.add_option(
1129        '--source-context',
1130        '-C',
1131        type='int',
1132        metavar='NLINES',
1133        dest='source_context',
1134        help='show NLINES source lines of source context (default = 4)',
1135        default=4)
1136    option_parser.add_option(
1137        '--source-frames',
1138        type='int',
1139        metavar='NFRAMES',
1140        dest='source_frames',
1141        help='show source for NFRAMES (default = 4)',
1142        default=4)
1143    option_parser.add_option(
1144        '--source-all',
1145        action='store_true',
1146        dest='source_all',
1147        help='show source for all threads, not just the crashed thread',
1148        default=False)
1149    if add_interactive_options:
1150        option_parser.add_option(
1151            '-i',
1152            '--interactive',
1153            action='store_true',
1154            help='parse all crash logs and enter interactive mode',
1155            default=False)
1156    return option_parser
1157
1158
1159def SymbolicateCrashLogs(debugger, command_args):
1160    description = '''Symbolicate one or more darwin crash log files to provide source file and line information,
1161inlined stack frames back to the concrete functions, and disassemble the location of the crash
1162for the first frame of the crashed thread.
1163If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
1164for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
1165created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
1166you to explore the program as if it were stopped at the locations described in the crash log and functions can
1167be disassembled and lookups can be performed using the addresses found in the crash log.'''
1168    option_parser = CreateSymbolicateCrashLogOptions(
1169        'crashlog', description, True)
1170    try:
1171        (options, args) = option_parser.parse_args(command_args)
1172    except:
1173        return
1174
1175    if options.debug:
1176        print('command_args = %s' % command_args)
1177        print('options', options)
1178        print('args', args)
1179
1180    if options.debug_delay > 0:
1181        print("Waiting %u seconds for debugger to attach..." % options.debug_delay)
1182        time.sleep(options.debug_delay)
1183    error = lldb.SBError()
1184
1185    if args:
1186        if options.interactive:
1187            interactive_crashlogs(debugger, options, args)
1188        else:
1189            for crash_log_file in args:
1190                crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose)
1191                SymbolicateCrashLog(crash_log, options)
1192if __name__ == '__main__':
1193    # Create a new debugger instance
1194    debugger = lldb.SBDebugger.Create()
1195    SymbolicateCrashLogs(debugger, sys.argv[1:])
1196    lldb.SBDebugger.Destroy(debugger)
1197elif getattr(lldb, 'debugger', None):
1198    lldb.debugger.HandleCommand(
1199        'command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
1200    lldb.debugger.HandleCommand(
1201        'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
1202