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