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