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