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