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