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