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