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