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