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                # "r12: 0x00007fff6b5939c8  r13: 0x0000000007000006  r14: 0x0000000000002a03  r15: 0x0000000000000c00"
320                reg_values = re.findall ('([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line);
321                for reg_value in reg_values:
322                    #print 'reg_value = "%s"' % reg_value
323                    (reg, value) = reg_value.split(': ')
324                    #print 'reg = "%s"' % reg
325                    #print 'value = "%s"' % value
326                    thread.registers[reg.strip()] = int(value, 0)
327            elif parse_mode == PARSE_MODE_SYSTEM:
328                self.system_profile.append(line)
329        f.close()
330
331    def dump(self):
332        print "Crash Log File: %s" % (self.path)
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        return None
345
346    def create_target(self):
347        #print 'crashlog.create_target()...'
348        target = symbolication.Symbolicator.create_target(self)
349        if target:
350            return target
351        # We weren't able to open the main executable as, but we can still symbolicate
352        print 'crashlog.create_target()...2'
353        if self.idents:
354            for ident in self.idents:
355                image = self.find_image_with_identifier (ident)
356                if image:
357                    target = image.create_target ()
358                    if target:
359                        return target # success
360        print 'crashlog.create_target()...3'
361        for image in self.images:
362            target = image.create_target ()
363            if target:
364                return target # success
365        print 'crashlog.create_target()...4'
366        print 'error: unable to locate any executables from the crash log'
367        return None
368
369
370def usage():
371    print "Usage: lldb-symbolicate.py [-n name] executable-image"
372    sys.exit(0)
373
374class Interactive(cmd.Cmd):
375    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
376    image_option_parser = None
377
378    def __init__(self, crash_logs):
379        cmd.Cmd.__init__(self)
380        self.use_rawinput = False
381        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
382        self.crash_logs = crash_logs
383        self.prompt = '% '
384
385    def default(self, line):
386        '''Catch all for unknown command, which will exit the interpreter.'''
387        print "uknown command: %s" % line
388        return True
389
390    def do_q(self, line):
391        '''Quit command'''
392        return True
393
394    def do_quit(self, line):
395        '''Quit command'''
396        return True
397
398    def do_symbolicate(self, line):
399        description='''Symbolicate one or more darwin crash log files by index to provide source file and line information,
400        inlined stack frames back to the concrete functions, and disassemble the location of the crash
401        for the first frame of the crashed thread.'''
402        option_parser = CreateSymbolicateCrashLogOptions ('symbolicate', description, False)
403        command_args = shlex.split(line)
404        try:
405            (options, args) = option_parser.parse_args(command_args)
406        except:
407            return
408
409        if args:
410            # We have arguments, they must valid be crash log file indexes
411            for idx_str in args:
412                idx = int(idx_str)
413                if idx < len(self.crash_logs):
414                    SymbolicateCrashLog (self.crash_logs[idx], options)
415                else:
416                    print 'error: crash log index %u is out of range' % (idx)
417        else:
418            # No arguments, symbolicate all crash logs using the options provided
419            for idx in range(len(self.crash_logs)):
420                SymbolicateCrashLog (self.crash_logs[idx], options)
421
422    def do_list(self, line=None):
423        '''Dump a list of all crash logs that are currently loaded.
424
425        USAGE: list'''
426        print '%u crash logs are loaded:' % len(self.crash_logs)
427        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
428            print '[%u] = %s' % (crash_log_idx, crash_log.path)
429
430    def do_image(self, line):
431        '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.'''
432        usage = "usage: %prog [options] <PATH> [PATH ...]"
433        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.'''
434        command_args = shlex.split(line)
435        if not self.image_option_parser:
436            self.image_option_parser = optparse.OptionParser(description=description, prog='image',usage=usage)
437            self.image_option_parser.add_option('-a', '--all', action='store_true', help='show all images', default=False)
438        try:
439            (options, args) = self.image_option_parser.parse_args(command_args)
440        except:
441            return
442
443        if args:
444            for image_path in args:
445                fullpath_search = image_path[0] == '/'
446                for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
447                    matches_found = 0
448                    for (image_idx, image) in enumerate(crash_log.images):
449                        if fullpath_search:
450                            if image.get_resolved_path() == image_path:
451                                matches_found += 1
452                                print '[%u] ' % (crash_log_idx), image
453                        else:
454                            image_basename = image.get_resolved_path_basename()
455                            if image_basename == image_path:
456                                matches_found += 1
457                                print '[%u] ' % (crash_log_idx), image
458                    if matches_found == 0:
459                        for (image_idx, image) in enumerate(crash_log.images):
460                            resolved_image_path = image.get_resolved_path()
461                            if resolved_image_path and string.find(image.get_resolved_path(), image_path) >= 0:
462                                print '[%u] ' % (crash_log_idx), image
463        else:
464            for crash_log in self.crash_logs:
465                for (image_idx, image) in enumerate(crash_log.images):
466                    print '[%u] %s' % (image_idx, image)
467        return False
468
469
470def interactive_crashlogs(options, args):
471    crash_log_files = list()
472    for arg in args:
473        for resolved_path in glob.glob(arg):
474            crash_log_files.append(resolved_path)
475
476    crash_logs = list();
477    for crash_log_file in crash_log_files:
478        #print 'crash_log_file = "%s"' % crash_log_file
479        crash_log = CrashLog(crash_log_file)
480        if crash_log.error:
481            print crash_log.error
482            continue
483        if options.debug:
484            crash_log.dump()
485        if not crash_log.images:
486            print 'error: no images in crash log "%s"' % (crash_log)
487            continue
488        else:
489            crash_logs.append(crash_log)
490
491    interpreter = Interactive(crash_logs)
492    # List all crash logs that were imported
493    interpreter.do_list()
494    interpreter.cmdloop()
495
496
497def save_crashlog(debugger, command, result, dict):
498    usage = "usage: %prog [options] <output-path>"
499    description='''Export the state of current target into a crashlog file'''
500    parser = optparse.OptionParser(description=description, prog='save_crashlog',usage=usage)
501    parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
502    try:
503        (options, args) = parser.parse_args(shlex.split(command))
504    except:
505        result.PutCString ("error: invalid options");
506        return
507    if len(args) != 1:
508        result.PutCString ("error: invalid arguments, a single output file is the only valid argument")
509        return
510    out_file = open(args[0], 'w')
511    if not out_file:
512        result.PutCString ("error: failed to open file '%s' for writing...", args[0]);
513        return
514    if lldb.target:
515        identifier = lldb.target.executable.basename
516        if lldb.process:
517            pid = lldb.process.id
518            if pid != lldb.LLDB_INVALID_PROCESS_ID:
519                out_file.write('Process:         %s [%u]\n' % (identifier, pid))
520        out_file.write('Path:            %s\n' % (lldb.target.executable.fullpath))
521        out_file.write('Identifier:      %s\n' % (identifier))
522        out_file.write('\nDate/Time:       %s\n' % (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
523        out_file.write('OS Version:      Mac OS X %s (%s)\n' % (platform.mac_ver()[0], commands.getoutput('sysctl -n kern.osversion')));
524        out_file.write('Report Version:  9\n')
525        for thread_idx in range(lldb.process.num_threads):
526            thread = lldb.process.thread[thread_idx]
527            out_file.write('\nThread %u:\n' % (thread_idx))
528            for (frame_idx, frame) in enumerate(thread.frames):
529                frame_pc = frame.pc
530                frame_offset = 0
531                if frame.function:
532                    block = frame.GetFrameBlock()
533                    block_range = block.range[frame.addr]
534                    if block_range:
535                        block_start_addr = block_range[0]
536                        frame_offset = frame_pc - block_start_addr.load_addr
537                    else:
538                        frame_offset = frame_pc - frame.function.addr.load_addr
539                elif frame.symbol:
540                    frame_offset = frame_pc - frame.symbol.addr.load_addr
541                out_file.write('%-3u %-32s 0x%16.16x %s' % (frame_idx, frame.module.file.basename, frame_pc, frame.name))
542                if frame_offset > 0:
543                    out_file.write(' + %u' % (frame_offset))
544                line_entry = frame.line_entry
545                if line_entry:
546                    if options.verbose:
547                        # This will output the fullpath + line + column
548                        out_file.write(' %s' % (line_entry))
549                    else:
550                        out_file.write(' %s:%u' % (line_entry.file.basename, line_entry.line))
551                        column = line_entry.column
552                        if column:
553                            out_file.write(':%u' % (column))
554                out_file.write('\n')
555
556        out_file.write('\nBinary Images:\n')
557        for module in lldb.target.modules:
558            text_segment = module.section['__TEXT']
559            if text_segment:
560                text_segment_load_addr = text_segment.GetLoadAddress(lldb.target)
561                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
562                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
563                    identifier = module.file.basename
564                    module_version = '???'
565                    module_version_array = module.GetVersion()
566                    if module_version_array:
567                        module_version = '.'.join(map(str,module_version_array))
568                    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))
569        out_file.close()
570    else:
571        result.PutCString ("error: invalid target");
572
573
574def Symbolicate(debugger, command, result, dict):
575    try:
576        SymbolicateCrashLogs (shlex.split(command))
577    except:
578        result.PutCString ("error: python exception %s" % sys.exc_info()[0])
579
580def SymbolicateCrashLog(crash_log, options):
581    if crash_log.error:
582        print crash_log.error
583        return
584    if options.debug:
585        crash_log.dump()
586    if not crash_log.images:
587        print 'error: no images in crash log'
588        return
589
590    if options.dump_image_list:
591        print "Binary Images:"
592        for image in crash_log.images:
593            if options.verbose:
594                print image.debug_dump()
595            else:
596                print image
597
598    target = crash_log.create_target ()
599    if not target:
600        return
601    exe_module = target.GetModuleAtIndex(0)
602    images_to_load = list()
603    loaded_images = list()
604    if options.load_all_images:
605        # --load-all option was specified, load everything up
606        for image in crash_log.images:
607            images_to_load.append(image)
608    else:
609        # Only load the images found in stack frames for the crashed threads
610        if options.crashed_only:
611            for thread in crash_log.threads:
612                if thread.did_crash():
613                    for ident in thread.idents:
614                        images = crash_log.find_images_with_identifier (ident)
615                        if images:
616                            for image in images:
617                                images_to_load.append(image)
618                        else:
619                            print 'error: can\'t find image for identifier "%s"' % ident
620        else:
621            for ident in crash_log.idents:
622                images = crash_log.find_images_with_identifier (ident)
623                if images:
624                    for image in images:
625                        images_to_load.append(image)
626                else:
627                    print 'error: can\'t find image for identifier "%s"' % ident
628
629    for image in images_to_load:
630        if image in loaded_images:
631            print "warning: skipping %s loaded at %#16.16x duplicate entry (probably commpage)" % (image.path, image.text_addr_lo)
632        else:
633            err = image.add_module (target)
634            if err:
635                print err
636            else:
637                #print 'loaded %s' % image
638                loaded_images.append(image)
639
640    for thread in crash_log.threads:
641        this_thread_crashed = thread.did_crash()
642        if options.crashed_only and this_thread_crashed == False:
643            continue
644        print "%s" % thread
645        #prev_frame_index = -1
646        display_frame_idx = -1
647        for frame_idx, frame in enumerate(thread.frames):
648            disassemble = (this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth;
649            if frame_idx == 0:
650                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc, options.verbose)
651            else:
652                # Any frame above frame zero and we have to subtract one to get the previous line entry
653                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc - 1, options.verbose)
654
655            if symbolicated_frame_addresses:
656                symbolicated_frame_address_idx = 0
657                for symbolicated_frame_address in symbolicated_frame_addresses:
658                    display_frame_idx += 1
659                    print '[%3u] %s' % (frame_idx, symbolicated_frame_address)
660                    if (options.source_all or thread.did_crash()) and display_frame_idx < options.source_frames and options.source_context:
661                        source_context = options.source_context
662                        line_entry = symbolicated_frame_address.get_symbol_context().line_entry
663                        if line_entry.IsValid():
664                            strm = lldb.SBStream()
665                            if line_entry:
666                                lldb.debugger.GetSourceManager().DisplaySourceLinesWithLineNumbers(line_entry.file, line_entry.line, source_context, source_context, "->", strm)
667                            source_text = strm.GetData()
668                            if source_text:
669                                # Indent the source a bit
670                                indent_str = '    '
671                                join_str = '\n' + indent_str
672                                print '%s%s' % (indent_str, join_str.join(source_text.split('\n')))
673                    if symbolicated_frame_address_idx == 0:
674                        if disassemble:
675                            instructions = symbolicated_frame_address.get_instructions()
676                            if instructions:
677                                print
678                                symbolication.disassemble_instructions (target,
679                                                                        instructions,
680                                                                        frame.pc,
681                                                                        options.disassemble_before,
682                                                                        options.disassemble_after, frame.index > 0)
683                                print
684                    symbolicated_frame_address_idx += 1
685            else:
686                print frame
687        print
688
689def CreateSymbolicateCrashLogOptions(command_name, description, add_interactive_options):
690    usage = "usage: %prog [options] <FILE> [FILE ...]"
691    option_parser = optparse.OptionParser(description=description, prog='crashlog',usage=usage)
692    option_parser.add_option('--verbose'       , '-v', action='store_true', dest='verbose', help='display verbose debug info', default=False)
693    option_parser.add_option('--debug'         , '-g', action='store_true', dest='debug', help='display verbose debug logging', default=False)
694    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)
695    option_parser.add_option('--images'        ,       action='store_true', dest='dump_image_list', help='show image list', default=False)
696    option_parser.add_option('--debug-delay'   ,       type='int', dest='debug_delay', metavar='NSEC', help='pause for NSEC seconds for debugger', default=0)
697    option_parser.add_option('--crashed-only'  , '-c', action='store_true', dest='crashed_only', help='only symbolicate the crashed thread', default=False)
698    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)
699    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)
700    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)
701    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)
702    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)
703    option_parser.add_option('--source-frames' ,       type='int', metavar='NFRAMES', dest='source_frames', help='show source for NFRAMES (default = 4)', default=4)
704    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)
705    if add_interactive_options:
706        option_parser.add_option('-i', '--interactive', action='store_true', help='parse all crash logs and enter interactive mode', default=False)
707    return option_parser
708
709def SymbolicateCrashLogs(command_args):
710    description='''Symbolicate one or more darwin crash log files to provide source file and line information,
711inlined stack frames back to the concrete functions, and disassemble the location of the crash
712for the first frame of the crashed thread.
713If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
714for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
715created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
716you to explore the program as if it were stopped at the locations described in the crash log and functions can
717be disassembled and lookups can be performed using the addresses found in the crash log.'''
718    option_parser = CreateSymbolicateCrashLogOptions ('crashlog', description, True)
719    try:
720        (options, args) = option_parser.parse_args(command_args)
721    except:
722        return
723
724    if options.debug:
725        print 'command_args = %s' % command_args
726        print 'options', options
727        print 'args', args
728
729    if options.debug_delay > 0:
730        print "Waiting %u seconds for debugger to attach..." % options.debug_delay
731        time.sleep(options.debug_delay)
732    error = lldb.SBError()
733
734    if args:
735        if options.interactive:
736            interactive_crashlogs(options, args)
737        else:
738            for crash_log_file in args:
739                crash_log = CrashLog(crash_log_file)
740                SymbolicateCrashLog (crash_log, options)
741if __name__ == '__main__':
742    # Create a new debugger instance
743    print 'main'
744    lldb.debugger = lldb.SBDebugger.Create()
745    SymbolicateCrashLogs (sys.argv[1:])
746elif getattr(lldb, 'debugger', None):
747    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
748    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
749    print '"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help'
750
751