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