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, exe_ctx, 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 = exe_ctx.target
713    if target:
714        identifier = target.executable.basename
715        process = exe_ctx.process
716        if process:
717            pid = process.id
718            if pid != lldb.LLDB_INVALID_PROCESS_ID:
719                out_file.write(
720                    'Process:         %s [%u]\n' %
721                    (identifier, pid))
722        out_file.write('Path:            %s\n' % (target.executable.fullpath))
723        out_file.write('Identifier:      %s\n' % (identifier))
724        out_file.write('\nDate/Time:       %s\n' %
725                       (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
726        out_file.write(
727            'OS Version:      Mac OS X %s (%s)\n' %
728            (platform.mac_ver()[0], commands.getoutput('sysctl -n kern.osversion')))
729        out_file.write('Report Version:  9\n')
730        for thread_idx in range(process.num_threads):
731            thread = process.thread[thread_idx]
732            out_file.write('\nThread %u:\n' % (thread_idx))
733            for (frame_idx, frame) in enumerate(thread.frames):
734                frame_pc = frame.pc
735                frame_offset = 0
736                if frame.function:
737                    block = frame.GetFrameBlock()
738                    block_range = block.range[frame.addr]
739                    if block_range:
740                        block_start_addr = block_range[0]
741                        frame_offset = frame_pc - block_start_addr.load_addr
742                    else:
743                        frame_offset = frame_pc - frame.function.addr.load_addr
744                elif frame.symbol:
745                    frame_offset = frame_pc - frame.symbol.addr.load_addr
746                out_file.write(
747                    '%-3u %-32s 0x%16.16x %s' %
748                    (frame_idx, frame.module.file.basename, frame_pc, frame.name))
749                if frame_offset > 0:
750                    out_file.write(' + %u' % (frame_offset))
751                line_entry = frame.line_entry
752                if line_entry:
753                    if options.verbose:
754                        # This will output the fullpath + line + column
755                        out_file.write(' %s' % (line_entry))
756                    else:
757                        out_file.write(
758                            ' %s:%u' %
759                            (line_entry.file.basename, line_entry.line))
760                        column = line_entry.column
761                        if column:
762                            out_file.write(':%u' % (column))
763                out_file.write('\n')
764
765        out_file.write('\nBinary Images:\n')
766        for module in target.modules:
767            text_segment = module.section['__TEXT']
768            if text_segment:
769                text_segment_load_addr = text_segment.GetLoadAddress(target)
770                if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS:
771                    text_segment_end_load_addr = text_segment_load_addr + text_segment.size
772                    identifier = module.file.basename
773                    module_version = '???'
774                    module_version_array = module.GetVersion()
775                    if module_version_array:
776                        module_version = '.'.join(
777                            map(str, module_version_array))
778                    out_file.write(
779                        '    0x%16.16x - 0x%16.16x  %s (%s - ???) <%s> %s\n' %
780                        (text_segment_load_addr,
781                         text_segment_end_load_addr,
782                         identifier,
783                         module_version,
784                         module.GetUUIDString(),
785                         module.file.fullpath))
786        out_file.close()
787    else:
788        result.PutCString("error: invalid target")
789
790
791def Symbolicate(debugger, command, result, dict):
792    try:
793        SymbolicateCrashLogs(shlex.split(command))
794    except:
795        result.PutCString("error: python exception %s" % sys.exc_info()[0])
796
797
798def SymbolicateCrashLog(crash_log, options):
799    if crash_log.error:
800        print crash_log.error
801        return
802    if options.debug:
803        crash_log.dump()
804    if not crash_log.images:
805        print 'error: no images in crash log'
806        return
807
808    if options.dump_image_list:
809        print "Binary Images:"
810        for image in crash_log.images:
811            if options.verbose:
812                print image.debug_dump()
813            else:
814                print image
815
816    target = crash_log.create_target()
817    if not target:
818        return
819    exe_module = target.GetModuleAtIndex(0)
820    images_to_load = list()
821    loaded_images = list()
822    if options.load_all_images:
823        # --load-all option was specified, load everything up
824        for image in crash_log.images:
825            images_to_load.append(image)
826    else:
827        # Only load the images found in stack frames for the crashed threads
828        if options.crashed_only:
829            for thread in crash_log.threads:
830                if thread.did_crash():
831                    for ident in thread.idents:
832                        images = crash_log.find_images_with_identifier(ident)
833                        if images:
834                            for image in images:
835                                images_to_load.append(image)
836                        else:
837                            print 'error: can\'t find image for identifier "%s"' % ident
838        else:
839            for ident in crash_log.idents:
840                images = crash_log.find_images_with_identifier(ident)
841                if images:
842                    for image in images:
843                        images_to_load.append(image)
844                else:
845                    print 'error: can\'t find image for identifier "%s"' % ident
846
847    for image in images_to_load:
848        if image not in loaded_images:
849            err = image.add_module(target)
850            if err:
851                print err
852            else:
853                # print 'loaded %s' % image
854                loaded_images.append(image)
855
856    if crash_log.backtraces:
857        for thread in crash_log.backtraces:
858            thread.dump_symbolicated(crash_log, options)
859            print
860
861    for thread in crash_log.threads:
862        thread.dump_symbolicated(crash_log, options)
863        print
864
865
866def CreateSymbolicateCrashLogOptions(
867        command_name,
868        description,
869        add_interactive_options):
870    usage = "usage: %prog [options] <FILE> [FILE ...]"
871    option_parser = optparse.OptionParser(
872        description=description, prog='crashlog', usage=usage)
873    option_parser.add_option(
874        '--verbose',
875        '-v',
876        action='store_true',
877        dest='verbose',
878        help='display verbose debug info',
879        default=False)
880    option_parser.add_option(
881        '--debug',
882        '-g',
883        action='store_true',
884        dest='debug',
885        help='display verbose debug logging',
886        default=False)
887    option_parser.add_option(
888        '--load-all',
889        '-a',
890        action='store_true',
891        dest='load_all_images',
892        help='load all executable images, not just the images found in the crashed stack frames',
893        default=False)
894    option_parser.add_option(
895        '--images',
896        action='store_true',
897        dest='dump_image_list',
898        help='show image list',
899        default=False)
900    option_parser.add_option(
901        '--debug-delay',
902        type='int',
903        dest='debug_delay',
904        metavar='NSEC',
905        help='pause for NSEC seconds for debugger',
906        default=0)
907    option_parser.add_option(
908        '--crashed-only',
909        '-c',
910        action='store_true',
911        dest='crashed_only',
912        help='only symbolicate the crashed thread',
913        default=False)
914    option_parser.add_option(
915        '--disasm-depth',
916        '-d',
917        type='int',
918        dest='disassemble_depth',
919        help='set the depth in stack frames that should be disassembled (default is 1)',
920        default=1)
921    option_parser.add_option(
922        '--disasm-all',
923        '-D',
924        action='store_true',
925        dest='disassemble_all_threads',
926        help='enabled disassembly of frames on all threads (not just the crashed thread)',
927        default=False)
928    option_parser.add_option(
929        '--disasm-before',
930        '-B',
931        type='int',
932        dest='disassemble_before',
933        help='the number of instructions to disassemble before the frame PC',
934        default=4)
935    option_parser.add_option(
936        '--disasm-after',
937        '-A',
938        type='int',
939        dest='disassemble_after',
940        help='the number of instructions to disassemble after the frame PC',
941        default=4)
942    option_parser.add_option(
943        '--source-context',
944        '-C',
945        type='int',
946        metavar='NLINES',
947        dest='source_context',
948        help='show NLINES source lines of source context (default = 4)',
949        default=4)
950    option_parser.add_option(
951        '--source-frames',
952        type='int',
953        metavar='NFRAMES',
954        dest='source_frames',
955        help='show source for NFRAMES (default = 4)',
956        default=4)
957    option_parser.add_option(
958        '--source-all',
959        action='store_true',
960        dest='source_all',
961        help='show source for all threads, not just the crashed thread',
962        default=False)
963    if add_interactive_options:
964        option_parser.add_option(
965            '-i',
966            '--interactive',
967            action='store_true',
968            help='parse all crash logs and enter interactive mode',
969            default=False)
970    return option_parser
971
972
973def SymbolicateCrashLogs(command_args):
974    description = '''Symbolicate one or more darwin crash log files to provide source file and line information,
975inlined stack frames back to the concrete functions, and disassemble the location of the crash
976for the first frame of the crashed thread.
977If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
978for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
979created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
980you to explore the program as if it were stopped at the locations described in the crash log and functions can
981be disassembled and lookups can be performed using the addresses found in the crash log.'''
982    option_parser = CreateSymbolicateCrashLogOptions(
983        'crashlog', description, True)
984    try:
985        (options, args) = option_parser.parse_args(command_args)
986    except:
987        return
988
989    if options.debug:
990        print 'command_args = %s' % command_args
991        print 'options', options
992        print 'args', args
993
994    if options.debug_delay > 0:
995        print "Waiting %u seconds for debugger to attach..." % options.debug_delay
996        time.sleep(options.debug_delay)
997    error = lldb.SBError()
998
999    if args:
1000        if options.interactive:
1001            interactive_crashlogs(options, args)
1002        else:
1003            for crash_log_file in args:
1004                crash_log = CrashLog(crash_log_file)
1005                SymbolicateCrashLog(crash_log, options)
1006if __name__ == '__main__':
1007    # Create a new debugger instance
1008    lldb.debugger = lldb.SBDebugger.Create()
1009    SymbolicateCrashLogs(sys.argv[1:])
1010    lldb.SBDebugger.Destroy(lldb.debugger)
1011elif getattr(lldb, 'debugger', None):
1012    lldb.debugger.HandleCommand(
1013        'command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
1014    lldb.debugger.HandleCommand(
1015        'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog')
1016    print '"crashlog" and "save_crashlog" command installed, use the "--help" option for detailed help'
1017