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