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