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