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