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