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