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