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