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