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