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