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