1#!/usr/bin/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
29import lldb
30import commands
31import cmd
32import datetime
33import glob
34import optparse
35import os
36import platform
37import plistlib
38import pprint # pp = pprint.PrettyPrinter(indent=4); pp.pprint(command_args)
39import re
40import shlex
41import string
42import sys
43import time
44import uuid
45from lldb.utils import symbolication
46
47PARSE_MODE_NORMAL = 0
48PARSE_MODE_THREAD = 1
49PARSE_MODE_IMAGES = 2
50PARSE_MODE_THREGS = 3
51PARSE_MODE_SYSTEM = 4
52
53class CrashLog(symbolication.Symbolicator):
54    """Class that does parses darwin crash logs"""
55    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
56    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
57    frame_regex = re.compile('^([0-9]+) +([^ ]+) *\t(0x[0-9a-fA-F]+) +(.*)')
58    image_regex_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^<]+)<([-0-9a-fA-F]+)> (.*)');
59    image_regex_no_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^/]+)/(.*)');
60    empty_line_regex = re.compile('^$')
61
62    class Thread:
63        """Class that represents a thread in a darwin crash log"""
64        def __init__(self, index):
65            self.index = index
66            self.frames = list()
67            self.registers = dict()
68            self.reason = None
69            self.queue = None
70
71        def dump(self, prefix):
72            print "%sThread[%u] %s" % (prefix, self.index, self.reason)
73            if self.frames:
74                print "%s  Frames:" % (prefix)
75                for frame in self.frames:
76                    frame.dump(prefix + '    ')
77            if self.registers:
78                print "%s  Registers:" % (prefix)
79                for reg in self.registers.keys():
80                    print "%s    %-5s = %#16.16x" % (prefix, reg, self.registers[reg])
81
82        def did_crash(self):
83            return self.reason != None
84
85        def __str__(self):
86            s = "Thread[%u]" % self.index
87            if self.reason:
88                s += ' %s' % self.reason
89            return s
90
91
92    class Frame:
93        """Class that represents a stack frame in a thread in a darwin crash log"""
94        def __init__(self, index, pc, description):
95            self.pc = pc
96            self.description = description
97            self.index = index
98
99        def __str__(self):
100            if self.description:
101                return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description)
102            else:
103                return "[%3u] 0x%16.16x" % (self.index, self.pc)
104
105        def dump(self, prefix):
106            print "%s%s" % (prefix, str(self))
107
108    class DarwinImage(symbolication.Image):
109        """Class that represents a binary images in a darwin crash log"""
110        dsymForUUIDBinary = os.path.expanduser('~rc/bin/dsymForUUID')
111        if not os.path.exists(dsymForUUIDBinary):
112            dsymForUUIDBinary = commands.getoutput('which dsymForUUID')
113
114        dwarfdump_uuid_regex = re.compile('UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
115
116        def __init__(self, text_addr_lo, text_addr_hi, identifier, version, uuid, path):
117            symbolication.Image.__init__(self, path, uuid);
118            self.add_section (symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT"))
119            self.identifier = identifier
120            self.version = version
121
122        def locate_module_and_debug_symbols(self):
123            # Don't load a module twice...
124            if self.resolved:
125                return True
126            # Mark this as resolved so we don't keep trying
127            self.resolved = True
128            uuid_str = self.get_normalized_uuid_string()
129            print 'Getting symbols for %s %s...' % (uuid_str, self.path),
130            if os.path.exists(self.dsymForUUIDBinary):
131                dsym_for_uuid_command = '%s %s' % (self.dsymForUUIDBinary, uuid_str)
132                s = commands.getoutput(dsym_for_uuid_command)
133                if s:
134                    plist_root = plistlib.readPlistFromString (s)
135                    if plist_root:
136                        plist = plist_root[uuid_str]
137                        if plist:
138                            if 'DBGArchitecture' in plist:
139                                self.arch = plist['DBGArchitecture']
140                            if 'DBGDSYMPath' in plist:
141                                self.symfile = os.path.realpath(plist['DBGDSYMPath'])
142                            if 'DBGSymbolRichExecutable' in plist:
143                                self.resolved_path = os.path.expanduser (plist['DBGSymbolRichExecutable'])
144            if not self.resolved_path and os.path.exists(self.path):
145                dwarfdump_cmd_output = commands.getoutput('dwarfdump --uuid "%s"' % self.path)
146                self_uuid = self.get_uuid()
147                for line in dwarfdump_cmd_output.splitlines():
148                    match = self.dwarfdump_uuid_regex.search (line)
149                    if match:
150                        dwarf_uuid_str = match.group(1)
151                        dwarf_uuid = uuid.UUID(dwarf_uuid_str)
152                        if self_uuid == dwarf_uuid:
153                            self.resolved_path = self.path
154                            self.arch = match.group(2)
155                            break;
156                if not self.resolved_path:
157                    self.unavailable = True
158                    print "error\n    error: unable to locate '%s' with UUID %s" % (self.path, uuid_str)
159                    return False
160            if (self.resolved_path and os.path.exists(self.resolved_path)) or (self.path and os.path.exists(self.path)):
161                print 'ok'
162                # if self.resolved_path:
163                #     print '  exe = "%s"' % self.resolved_path
164                # if self.symfile:
165                #     print ' dsym = "%s"' % self.symfile
166                return True
167            else:
168                self.unavailable = True
169            return False
170
171
172
173    def __init__(self, path):
174        """CrashLog constructor that take a path to a darwin crash log file"""
175        symbolication.Symbolicator.__init__(self);
176        self.path = os.path.expanduser(path);
177        self.info_lines = list()
178        self.system_profile = list()
179        self.threads = list()
180        self.idents = list() # A list of the required identifiers for doing all stack backtraces
181        self.crashed_thread_idx = -1
182        self.version = -1
183        self.error = None
184        # With possible initial component of ~ or ~user replaced by that user's home directory.
185        try:
186            f = open(self.path)
187        except IOError:
188            self.error = 'error: cannot open "%s"' % self.path
189            return
190
191        self.file_lines = f.read().splitlines()
192        parse_mode = PARSE_MODE_NORMAL
193        thread = None
194        for line in self.file_lines:
195            # print line
196            line_len = len(line)
197            if line_len == 0:
198                if thread:
199                    if parse_mode == PARSE_MODE_THREAD:
200                        if thread.index == self.crashed_thread_idx:
201                            thread.reason = ''
202                            if self.thread_exception:
203                                thread.reason += self.thread_exception
204                            if self.thread_exception_data:
205                                thread.reason += " (%s)" % self.thread_exception_data
206                        self.threads.append(thread)
207                    thread = None
208                else:
209                    # only append an extra empty line if the previous line
210                    # in the info_lines wasn't empty
211                    if len(self.info_lines) > 0 and len(self.info_lines[-1]):
212                        self.info_lines.append(line)
213                parse_mode = PARSE_MODE_NORMAL
214                # print 'PARSE_MODE_NORMAL'
215            elif parse_mode == PARSE_MODE_NORMAL:
216                if line.startswith ('Process:'):
217                    (self.process_name, pid_with_brackets) = line[8:].strip().split()
218                    self.process_id = pid_with_brackets.strip('[]')
219                elif line.startswith ('Path:'):
220                    self.process_path = line[5:].strip()
221                elif line.startswith ('Identifier:'):
222                    self.process_identifier = line[11:].strip()
223                elif line.startswith ('Version:'):
224                    version_string = line[8:].strip()
225                    matched_pair = re.search("(.+)\((.+)\)", version_string)
226                    if matched_pair:
227                        self.process_version = matched_pair.group(1)
228                        self.process_compatability_version = matched_pair.group(2)
229                    else:
230                        self.process = version_string
231                        self.process_compatability_version = version_string
232                elif line.startswith ('Parent Process:'):
233                    (self.parent_process_name, pid_with_brackets) = line[15:].strip().split()
234                    self.parent_process_id = pid_with_brackets.strip('[]')
235                elif line.startswith ('Exception Type:'):
236                    self.thread_exception = line[15:].strip()
237                    continue
238                elif line.startswith ('Exception Codes:'):
239                    self.thread_exception_data = line[16:].strip()
240                    continue
241                elif line.startswith ('Crashed Thread:'):
242                    self.crashed_thread_idx = int(line[15:].strip().split()[0])
243                    continue
244                elif line.startswith ('Report Version:'):
245                    self.version = int(line[15:].strip())
246                    continue
247                elif line.startswith ('System Profile:'):
248                    parse_mode = PARSE_MODE_SYSTEM
249                    continue
250                elif (line.startswith ('Interval Since Last Report:') or
251                      line.startswith ('Crashes Since Last Report:') or
252                      line.startswith ('Per-App Interval Since Last Report:') or
253                      line.startswith ('Per-App Crashes Since Last Report:') or
254                      line.startswith ('Sleep/Wake UUID:') or
255                      line.startswith ('Anonymous UUID:')):
256                    # ignore these
257                    continue
258                elif line.startswith ('Thread'):
259                    thread_state_match = self.thread_state_regex.search (line)
260                    if thread_state_match:
261                        thread_state_match = self.thread_regex.search (line)
262                        thread_idx = int(thread_state_match.group(1))
263                        parse_mode = PARSE_MODE_THREGS
264                        thread = self.threads[thread_idx]
265                    else:
266                        thread_match = self.thread_regex.search (line)
267                        if thread_match:
268                            # print 'PARSE_MODE_THREAD'
269                            parse_mode = PARSE_MODE_THREAD
270                            thread_idx = int(thread_match.group(1))
271                            thread = CrashLog.Thread(thread_idx)
272                    continue
273                elif line.startswith ('Binary Images:'):
274                    parse_mode = PARSE_MODE_IMAGES
275                    continue
276                self.info_lines.append(line.strip())
277            elif parse_mode == PARSE_MODE_THREAD:
278                if line.startswith ('Thread'):
279                    continue
280                frame_match = self.frame_regex.search(line)
281                if frame_match:
282                    ident = frame_match.group(2)
283                    if not ident in self.idents:
284                        self.idents.append(ident)
285                    thread.frames.append (CrashLog.Frame(int(frame_match.group(1)), int(frame_match.group(3), 0), frame_match.group(4)))
286                else:
287                    print 'error: frame regex failed for line: "%s"' % line
288            elif parse_mode == PARSE_MODE_IMAGES:
289                image_match = self.image_regex_uuid.search (line)
290                if image_match:
291                    image = CrashLog.DarwinImage (int(image_match.group(1),0),
292                                                  int(image_match.group(2),0),
293                                                  image_match.group(3).strip(),
294                                                  image_match.group(4).strip(),
295                                                  uuid.UUID(image_match.group(5)),
296                                                  image_match.group(6))
297                    self.images.append (image)
298                else:
299                    image_match = self.image_regex_no_uuid.search (line)
300                    if image_match:
301                        image = CrashLog.DarwinImage (int(image_match.group(1),0),
302                                                      int(image_match.group(2),0),
303                                                      image_match.group(3).strip(),
304                                                      image_match.group(4).strip(),
305                                                      None,
306                                                      image_match.group(5))
307                        self.images.append (image)
308                    else:
309                        print "error: image regex failed for: %s" % line
310
311            elif parse_mode == PARSE_MODE_THREGS:
312                stripped_line = line.strip()
313                reg_values = re.split('  +', stripped_line);
314                for reg_value in reg_values:
315                    #print 'reg_value = "%s"' % reg_value
316                    (reg, value) = reg_value.split(': ')
317                    #print 'reg = "%s"' % reg
318                    #print 'value = "%s"' % value
319                    thread.registers[reg.strip()] = int(value, 0)
320            elif parse_mode == PARSE_MODE_SYSTEM:
321                self.system_profile.append(line)
322        f.close()
323
324    def dump(self):
325        print "Crash Log File: %s" % (self.path)
326        print "\nThreads:"
327        for thread in self.threads:
328            thread.dump('  ')
329        print "\nImages:"
330        for image in self.images:
331            image.dump('  ')
332
333    def find_image_with_identifier(self, identifier):
334        for image in self.images:
335            if image.identifier == identifier:
336                return image
337        return None
338
339    def create_target(self):
340        #print 'crashlog.create_target()...'
341        target = symbolication.Symbolicator.create_target(self)
342        if target:
343            return target
344        # We weren't able to open the main executable as, but we can still symbolicate
345        print 'crashlog.create_target()...2'
346        if self.idents:
347            for ident in self.idents:
348                image = self.find_image_with_identifier (ident)
349                if image:
350                    target = image.create_target ()
351                    if target:
352                        return target # success
353        print 'crashlog.create_target()...3'
354        for image in self.images:
355            target = image.create_target ()
356            if target:
357                return target # success
358        print 'crashlog.create_target()...4'
359        print 'error: unable to locate any executables from the crash log'
360        return None
361
362
363def usage():
364    print "Usage: lldb-symbolicate.py [-n name] executable-image"
365    sys.exit(0)
366
367class Interactive(cmd.Cmd):
368    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
369    image_option_parser = None
370
371    def __init__(self, crash_logs):
372        cmd.Cmd.__init__(self)
373        self.use_rawinput = False
374        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
375        self.crash_logs = crash_logs
376        self.prompt = '% '
377
378    def default(self, line):
379        '''Catch all for unknown command, which will exit the interpreter.'''
380        print "uknown command: %s" % line
381        return True
382
383    def do_q(self, line):
384        '''Quit command'''
385        return True
386
387    def do_quit(self, line):
388        '''Quit command'''
389        return True
390
391    def do_symbolicate(self, line):
392        description='''Symbolicate one or more darwin crash log files by index to provide source file and line information,
393        inlined stack frames back to the concrete functions, and disassemble the location of the crash
394        for the first frame of the crashed thread.'''
395        option_parser = CreateSymbolicateCrashLogOptions ('symbolicate', description, False)
396        command_args = shlex.split(line)
397        try:
398            (options, args) = option_parser.parse_args(command_args)
399        except:
400            return
401
402        for idx_str in args:
403            idx = int(idx_str)
404            if idx < len(self.crash_logs):
405                SymbolicateCrashLog (self.crash_logs[idx], options)
406            else:
407                print 'error: crash log index %u is out of range' % (idx)
408
409    def do_list(self, line=None):
410        '''Dump a list of all crash logs that are currently loaded.
411
412        USAGE: list'''
413        print '%u crash logs are loaded:' % len(self.crash_logs)
414        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
415            print '[%u] = %s' % (crash_log_idx, crash_log.path)
416
417    def do_image(self, line):
418        '''Dump information about an image in the crash log given an image basename.
419
420        USAGE: image <basename>'''
421        usage = "usage: %prog [options] <PATH> [PATH ...]"
422        description='''Dump information about one or more images in all crash logs. The <PATH>
423        can be a full path or a image basename.'''
424        command_args = shlex.split(line)
425        if not self.image_option_parser:
426            self.image_option_parser = optparse.OptionParser(description=description, prog='image',usage=usage)
427            self.image_option_parser.add_option('-a', '--all', action='store_true', help='show all images', default=False)
428        try:
429            (options, args) = self.image_option_parser.parse_args(command_args)
430        except:
431            return
432
433        if args:
434            for image_path in args:
435                fullpath_search = image_path[0] == '/'
436                for crash_log in self.crash_logs:
437                    matches_found = 0
438                    for (image_idx, image) in enumerate(crash_log.images):
439                        if fullpath_search:
440                            if image.get_resolved_path() == image_path:
441                                matches_found += 1
442                                print image
443                        else:
444                            image_basename = image.get_resolved_path_basename()
445                            if image_basename == image_path:
446                                matches_found += 1
447                                print image
448                    if matches_found == 0:
449                        for (image_idx, image) in enumerate(crash_log.images):
450                            resolved_image_path = image.get_resolved_path()
451                            if resolved_image_path and string.find(image.get_resolved_path(), image_path) >= 0:
452                                print image
453        else:
454            for crash_log in self.crash_logs:
455                for (image_idx, image) in enumerate(crash_log.images):
456                    print '[%u] %s' % (image_idx, image)
457        return False
458
459
460def interactive_crashlogs(options, args):
461    crash_log_files = list()
462    for arg in args:
463        for resolved_path in glob.glob(arg):
464            crash_log_files.append(resolved_path)
465
466    crash_logs = list();
467    for crash_log_file in crash_log_files:
468        #print 'crash_log_file = "%s"' % crash_log_file
469        crash_log = CrashLog(crash_log_file)
470        if crash_log.error:
471            print crash_log.error
472            continue
473        if options.debug:
474            crash_log.dump()
475        if not crash_log.images:
476            print 'error: no images in crash log "%s"' % (crash_log)
477            continue
478        else:
479            crash_logs.append(crash_log)
480
481    interpreter = Interactive(crash_logs)
482    # List all crash logs that were imported
483    interpreter.do_list()
484    interpreter.cmdloop()
485
486
487def save_crashlog(debugger, command, result, dict):
488    usage = "usage: %prog [options] <output-path>"
489    description='''Export the state of current target into a crashlog file'''
490    parser = optparse.OptionParser(description=description, prog='save_crashlog',usage=usage)
491    parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
492    try:
493        (options, args) = parser.parse_args(shlex.split(command))
494    except:
495        result.PutCString ("error: invalid options");
496        return
497    if len(args) != 1:
498        result.PutCString ("error: invalid arguments, a single output file is the only valid argument")
499        return
500    out_file = open(args[0], 'w')
501    if not out_file:
502        result.PutCString ("error: failed to open file '%s' for writing...", args[0]);
503        return
504    if lldb.target:
505        identifier = lldb.target.executable.basename
506        if lldb.process:
507            pid = lldb.process.id
508            if pid != lldb.LLDB_INVALID_PROCESS_ID:
509                out_file.write('Process:         %s [%u]\n' % (identifier, pid))
510        out_file.write('Path:            %s\n' % (lldb.target.executable.fullpath))
511        out_file.write('Identifier:      %s\n' % (identifier))
512        out_file.write('\nDate/Time:       %s\n' % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
513        out_file.write('OS Version:      Mac OS X %s (%s)\n' % (platform.mac_ver()[0], commands.getoutput('sysctl -n kern.osversion')));
514        out_file.write('Report Version:  9\n')
515        for thread_idx in range(lldb.process.num_threads):
516            thread = lldb.process.thread[thread_idx]
517            out_file.write('\nThread %u:\n' % (thread_idx))
518            for (frame_idx, frame) in enumerate(thread.frames):
519                frame_pc = frame.pc
520                frame_offset = 0
521                if frame.function:
522                    block = frame.GetFrameBlock()
523                    block_range = block.range[frame.addr]
524                    if block_range:
525                        block_start_addr = block_range[0]
526                        frame_offset = frame_pc - block_start_addr.load_addr
527                    else:
528                        frame_offset = frame_pc - frame.function.addr.load_addr
529                elif frame.symbol:
530                    frame_offset = frame_pc - frame.symbol.addr.load_addr
531                out_file.write('%-3u %-32s 0x%16.16x %s' % (frame_idx, frame.module.file.basename, frame_pc, frame.name))
532                if frame_offset > 0:
533                    out_file.write(' + %u' % (frame_offset))
534                line_entry = frame.line_entry
535                if line_entry:
536                    if options.verbose:
537                        # This will output the fullpath + line + column
538                        out_file.write(' %s' % (line_entry))
539                    else:
540                        out_file.write(' %s:%u' % (line_entry.file.basename, line_entry.line))
541                        column = line_entry.column
542                        if column:
543                            out_file.write(':%u' % (column))
544                out_file.write('\n')
545
546        out_file.write('\nBinary Images:\n')
547        for module in lldb.target.modules:
548            text_segment = module.section['__TEXT']
549            if text_segment:
550                text_segment_load_addr = text_segment.GetLoadAddress(lldb.target)
551                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
552                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
553                    identifier = module.file.basename
554                    module_version = '???'
555                    module_version_array = module.GetVersion()
556                    if module_version_array:
557                        module_version = '.'.join(map(str,module_version_array))
558                    out_file.write ('    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' % (text_segment_load_addr, text_segment_end_load_addr, identifier, module_version, module.GetUUIDString(), module.file.fullpath))
559        out_file.close()
560    else:
561        result.PutCString ("error: invalid target");
562
563
564def Symbolicate(debugger, command, result, dict):
565    try:
566        SymbolicateCrashLogs (shlex.split(command))
567    except:
568        result.PutCString ("error: python exception %s" % sys.exc_info()[0])
569
570def SymbolicateCrashLog(crash_log, options):
571    if crash_log.error:
572        print crash_log.error
573        return
574    if options.debug:
575        crash_log.dump()
576    if not crash_log.images:
577        print 'error: no images in crash log'
578        return
579
580    target = crash_log.create_target ()
581    if not target:
582        return
583    exe_module = target.GetModuleAtIndex(0)
584    images_to_load = list()
585    loaded_images = list()
586    if options.load_all_images:
587        # --load-all option was specified, load everything up
588        for image in crash_log.images:
589            images_to_load.append(image)
590    else:
591        # Only load the images found in stack frames for the crashed threads
592        for ident in crash_log.idents:
593            images = crash_log.find_images_with_identifier (ident)
594            if images:
595                for image in images:
596                    images_to_load.append(image)
597            else:
598                print 'error: can\'t find image for identifier "%s"' % ident
599
600    for image in images_to_load:
601        if image in loaded_images:
602            print "warning: skipping %s loaded at %#16.16x duplicate entry (probably commpage)" % (image.path, image.text_addr_lo)
603        else:
604            err = image.add_module (target)
605            if err:
606                print err
607            else:
608                #print 'loaded %s' % image
609                loaded_images.append(image)
610
611    for thread in crash_log.threads:
612        this_thread_crashed = thread.did_crash()
613        if options.crashed_only and this_thread_crashed == False:
614            continue
615        print "%s" % thread
616        #prev_frame_index = -1
617        for frame_idx, frame in enumerate(thread.frames):
618            disassemble = (this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth;
619            if frame_idx == 0:
620                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc, options.verbose)
621            else:
622                # Any frame above frame zero and we have to subtract one to get the previous line entry
623                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc - 1, options.verbose)
624
625            if symbolicated_frame_addresses:
626                symbolicated_frame_address_idx = 0
627                for symbolicated_frame_address in symbolicated_frame_addresses:
628                    print '[%3u] %s' % (frame_idx, symbolicated_frame_address)
629
630                    if symbolicated_frame_address_idx == 0:
631                        if disassemble:
632                            instructions = symbolicated_frame_address.get_instructions()
633                            if instructions:
634                                print
635                                symbolication.disassemble_instructions (target,
636                                                                        instructions,
637                                                                        frame.pc,
638                                                                        options.disassemble_before,
639                                                                        options.disassemble_after, frame.index > 0)
640                                print
641                    symbolicated_frame_address_idx += 1
642            else:
643                print frame
644        print
645
646    if options.dump_image_list:
647        print "Binary Images:"
648        for image in crash_log.images:
649            print image
650
651def CreateSymbolicateCrashLogOptions(command_name, description, add_interactive_options):
652    usage = "usage: %prog [options] <FILE> [FILE ...]"
653    option_parser = optparse.OptionParser(description=description, prog='crashlog',usage=usage)
654    option_parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
655    option_parser.add_option('-g', '--debug', action='store_true', dest='debug', help='display verbose debug logging', default=False)
656    option_parser.add_option('-a', '--load-all', action='store_true', dest='load_all_images', help='load all executable images, not just the images found in the crashed stack frames', default=False)
657    option_parser.add_option('--images', action='store_true', dest='dump_image_list', help='show image list', default=False)
658    option_parser.add_option('--debug-delay', type='int', dest='debug_delay', metavar='NSEC', help='pause for NSEC seconds for debugger', default=0)
659    option_parser.add_option('-c', '--crashed-only', action='store_true', dest='crashed_only', help='only symbolicate the crashed thread', default=False)
660    option_parser.add_option('-d', '--disasm-depth', type='int', dest='disassemble_depth', help='set the depth in stack frames that should be disassembled (default is 1)', default=1)
661    option_parser.add_option('-D', '--disasm-all', action='store_true', dest='disassemble_all_threads', help='enabled disassembly of frames on all threads (not just the crashed thread)', default=False)
662    option_parser.add_option('-B', '--disasm-before', type='int', dest='disassemble_before', help='the number of instructions to disassemble before the frame PC', default=4)
663    option_parser.add_option('-A', '--disasm-after', type='int', dest='disassemble_after', help='the number of instructions to disassemble after the frame PC', default=4)
664    if add_interactive_options:
665        option_parser.add_option('-i', '--interactive', action='store_true', help='parse all crash logs and enter interactive mode', default=False)
666    return option_parser
667
668def SymbolicateCrashLogs(command_args):
669    description='''Symbolicate one or more darwin crash log files to provide source file and line information,
670inlined stack frames back to the concrete functions, and disassemble the location of the crash
671for the first frame of the crashed thread.
672If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
673for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
674created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
675you to explore the program as if it were stopped at the locations described in the crash log and functions can
676be disassembled and lookups can be performed using the addresses found in the crash log.'''
677    option_parser = CreateSymbolicateCrashLogOptions ('crashlog', description, True)
678    try:
679        (options, args) = option_parser.parse_args(command_args)
680    except:
681        return
682
683    if options.debug:
684        print 'command_args = %s' % command_args
685        print 'options', options
686        print 'args', args
687
688    if options.debug_delay > 0:
689        print "Waiting %u seconds for debugger to attach..." % options.debug_delay
690        time.sleep(options.debug_delay)
691    error = lldb.SBError()
692
693    if args:
694        if options.interactive:
695            interactive_crashlogs(options, args)
696        else:
697            for crash_log_file in args:
698                crash_log = CrashLog(crash_log_file)
699                SymbolicateCrashLog (crash_log, options)
700if __name__ == '__main__':
701    # Create a new debugger instance
702    print 'main'
703    lldb.debugger = lldb.SBDebugger.Create()
704    SymbolicateCrashLogs (sys.argv[1:])
705elif getattr(lldb, 'debugger', None):
706    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
707    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
708    print '"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help'
709
710