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