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