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 CrashLogParseException(Exception): 387 pass 388 389 390class CrashLogParser: 391 def parse(self, debugger, path, verbose): 392 try: 393 return JSONCrashLogParser(debugger, path, verbose).parse() 394 except CrashLogFormatException: 395 return TextCrashLogParser(debugger, path, verbose).parse() 396 397 398class JSONCrashLogParser: 399 def __init__(self, debugger, path, verbose): 400 self.path = os.path.expanduser(path) 401 self.verbose = verbose 402 self.crashlog = CrashLog(debugger, self.path, self.verbose) 403 404 def parse(self): 405 with open(self.path, 'r') as f: 406 buffer = f.read() 407 408 # First line is meta-data. 409 buffer = buffer[buffer.index('\n') + 1:] 410 411 try: 412 self.data = json.loads(buffer) 413 except ValueError: 414 raise CrashLogFormatException() 415 416 try: 417 self.parse_process_info(self.data) 418 self.parse_images(self.data['usedImages']) 419 self.parse_threads(self.data['threads']) 420 thread = self.crashlog.threads[self.crashlog.crashed_thread_idx] 421 reason = self.parse_crash_reason(self.data['exception']) 422 if thread.reason: 423 thread.reason = '{} {}'.format(thread.reason, reason) 424 else: 425 thread.reason = reason 426 except (KeyError, ValueError, TypeError) as e: 427 raise CrashLogParseException( 428 'Failed to parse JSON crashlog: {}: {}'.format( 429 type(e).__name__, e)) 430 431 return self.crashlog 432 433 def get_used_image(self, idx): 434 return self.data['usedImages'][idx] 435 436 def parse_process_info(self, json_data): 437 self.crashlog.process_id = json_data['pid'] 438 self.crashlog.process_identifier = json_data['procName'] 439 self.crashlog.process_path = json_data['procPath'] 440 441 def parse_crash_reason(self, json_exception): 442 exception_type = json_exception['type'] 443 exception_signal = json_exception['signal'] 444 if 'codes' in json_exception: 445 exception_extra = " ({})".format(json_exception['codes']) 446 elif 'subtype' in json_exception: 447 exception_extra = " ({})".format(json_exception['subtype']) 448 else: 449 exception_extra = "" 450 return "{} ({}){}".format(exception_type, exception_signal, 451 exception_extra) 452 453 def parse_images(self, json_images): 454 idx = 0 455 for json_image in json_images: 456 img_uuid = uuid.UUID(json_image['uuid']) 457 low = int(json_image['base']) 458 high = int(0) 459 name = json_image['name'] if 'name' in json_image else '' 460 path = json_image['path'] if 'path' in json_image else '' 461 version = '' 462 darwin_image = self.crashlog.DarwinImage(low, high, name, version, 463 img_uuid, path, 464 self.verbose) 465 self.crashlog.images.append(darwin_image) 466 idx += 1 467 468 def parse_frames(self, thread, json_frames): 469 idx = 0 470 for json_frame in json_frames: 471 image_id = int(json_frame['imageIndex']) 472 ident = self.get_used_image(image_id)['name'] 473 thread.add_ident(ident) 474 if ident not in self.crashlog.idents: 475 self.crashlog.idents.append(ident) 476 477 frame_offset = int(json_frame['imageOffset']) 478 image_addr = self.get_used_image(image_id)['base'] 479 pc = image_addr + frame_offset 480 thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset)) 481 idx += 1 482 483 def parse_threads(self, json_threads): 484 idx = 0 485 for json_thread in json_threads: 486 thread = self.crashlog.Thread(idx, False) 487 if 'name' in json_thread: 488 thread.reason = json_thread['name'] 489 if json_thread.get('triggered', False): 490 self.crashlog.crashed_thread_idx = idx 491 self.registers = self.parse_thread_registers( 492 json_thread['threadState']) 493 thread.queue = json_thread.get('queue') 494 self.parse_frames(thread, json_thread.get('frames', [])) 495 self.crashlog.threads.append(thread) 496 idx += 1 497 498 def parse_thread_registers(self, json_thread_state): 499 idx = 0 500 registers = dict() 501 for json_reg in json_thread_state.get('x', []): 502 key = str('x{}'.format(idx)) 503 value = int(json_reg['value']) 504 registers[key] = value 505 idx += 1 506 507 for register in ['lr', 'cpsr', 'fp', 'sp', 'esr', 'pc']: 508 if register in json_thread_state: 509 json_reg = json_thread_state[register] 510 registers[register] = int(json_reg['value']) 511 512 return registers 513 514 515class CrashLogParseMode: 516 NORMAL = 0 517 THREAD = 1 518 IMAGES = 2 519 THREGS = 3 520 SYSTEM = 4 521 INSTRS = 5 522 523 524class TextCrashLogParser: 525 parent_process_regex = re.compile('^Parent Process:\s*(.*)\[(\d+)\]') 526 thread_state_regex = re.compile('^Thread ([0-9]+) crashed with') 527 thread_instrs_regex = re.compile('^Thread ([0-9]+) instruction stream') 528 thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)') 529 app_backtrace_regex = re.compile('^Application Specific Backtrace ([0-9]+)([^:]*):(.*)') 530 version = r'(\(.+\)|(arm|x86_)[0-9a-z]+)\s+' 531 frame_regex = re.compile(r'^([0-9]+)' r'\s' # id 532 r'+(.+?)' r'\s+' # img_name 533 r'(' +version+ r')?' # img_version 534 r'(0x[0-9a-fA-F]{7}[0-9a-fA-F]+)' # addr 535 r' +(.*)' # offs 536 ) 537 null_frame_regex = re.compile(r'^([0-9]+)\s+\?\?\?\s+(0{7}0+) +(.*)') 538 image_regex_uuid = re.compile(r'(0x[0-9a-fA-F]+)' # img_lo 539 r'\s+' '-' r'\s+' # - 540 r'(0x[0-9a-fA-F]+)' r'\s+' # img_hi 541 r'[+]?(.+?)' r'\s+' # img_name 542 r'(' +version+ ')?' # img_version 543 r'(<([-0-9a-fA-F]+)>\s+)?' # img_uuid 544 r'(/.*)' # img_path 545 ) 546 547 548 def __init__(self, debugger, path, verbose): 549 self.path = os.path.expanduser(path) 550 self.verbose = verbose 551 self.thread = None 552 self.app_specific_backtrace = False 553 self.crashlog = CrashLog(debugger, self.path, self.verbose) 554 self.parse_mode = CrashLogParseMode.NORMAL 555 self.parsers = { 556 CrashLogParseMode.NORMAL : self.parse_normal, 557 CrashLogParseMode.THREAD : self.parse_thread, 558 CrashLogParseMode.IMAGES : self.parse_images, 559 CrashLogParseMode.THREGS : self.parse_thread_registers, 560 CrashLogParseMode.SYSTEM : self.parse_system, 561 CrashLogParseMode.INSTRS : self.parse_instructions, 562 } 563 564 def parse(self): 565 with open(self.path,'r') as f: 566 lines = f.read().splitlines() 567 568 for line in lines: 569 line_len = len(line) 570 if line_len == 0: 571 if self.thread: 572 if self.parse_mode == CrashLogParseMode.THREAD: 573 if self.thread.index == self.crashlog.crashed_thread_idx: 574 self.thread.reason = '' 575 if self.crashlog.thread_exception: 576 self.thread.reason += self.crashlog.thread_exception 577 if self.crashlog.thread_exception_data: 578 self.thread.reason += " (%s)" % self.crashlog.thread_exception_data 579 if self.app_specific_backtrace: 580 self.crashlog.backtraces.append(self.thread) 581 else: 582 self.crashlog.threads.append(self.thread) 583 self.thread = None 584 else: 585 # only append an extra empty line if the previous line 586 # in the info_lines wasn't empty 587 if len(self.crashlog.info_lines) > 0 and len(self.crashlog.info_lines[-1]): 588 self.crashlog.info_lines.append(line) 589 self.parse_mode = CrashLogParseMode.NORMAL 590 else: 591 self.parsers[self.parse_mode](line) 592 593 return self.crashlog 594 595 596 def parse_normal(self, line): 597 if line.startswith('Process:'): 598 (self.crashlog.process_name, pid_with_brackets) = line[ 599 8:].strip().split(' [') 600 self.crashlog.process_id = pid_with_brackets.strip('[]') 601 elif line.startswith('Path:'): 602 self.crashlog.process_path = line[5:].strip() 603 elif line.startswith('Identifier:'): 604 self.crashlog.process_identifier = line[11:].strip() 605 elif line.startswith('Version:'): 606 version_string = line[8:].strip() 607 matched_pair = re.search("(.+)\((.+)\)", version_string) 608 if matched_pair: 609 self.crashlog.process_version = matched_pair.group(1) 610 self.crashlog.process_compatability_version = matched_pair.group( 611 2) 612 else: 613 self.crashlog.process = version_string 614 self.crashlog.process_compatability_version = version_string 615 elif self.parent_process_regex.search(line): 616 parent_process_match = self.parent_process_regex.search( 617 line) 618 self.crashlog.parent_process_name = parent_process_match.group(1) 619 self.crashlog.parent_process_id = parent_process_match.group(2) 620 elif line.startswith('Exception Type:'): 621 self.crashlog.thread_exception = line[15:].strip() 622 return 623 elif line.startswith('Exception Codes:'): 624 self.crashlog.thread_exception_data = line[16:].strip() 625 return 626 elif line.startswith('Exception Subtype:'): # iOS 627 self.crashlog.thread_exception_data = line[18:].strip() 628 return 629 elif line.startswith('Crashed Thread:'): 630 self.crashlog.crashed_thread_idx = int(line[15:].strip().split()[0]) 631 return 632 elif line.startswith('Triggered by Thread:'): # iOS 633 self.crashlog.crashed_thread_idx = int(line[20:].strip().split()[0]) 634 return 635 elif line.startswith('Report Version:'): 636 self.crashlog.version = int(line[15:].strip()) 637 return 638 elif line.startswith('System Profile:'): 639 self.parse_mode = CrashLogParseMode.SYSTEM 640 return 641 elif (line.startswith('Interval Since Last Report:') or 642 line.startswith('Crashes Since Last Report:') or 643 line.startswith('Per-App Interval Since Last Report:') or 644 line.startswith('Per-App Crashes Since Last Report:') or 645 line.startswith('Sleep/Wake UUID:') or 646 line.startswith('Anonymous UUID:')): 647 # ignore these 648 return 649 elif line.startswith('Thread'): 650 thread_state_match = self.thread_state_regex.search(line) 651 if thread_state_match: 652 self.app_specific_backtrace = False 653 thread_state_match = self.thread_regex.search(line) 654 thread_idx = int(thread_state_match.group(1)) 655 self.parse_mode = CrashLogParseMode.THREGS 656 self.thread = self.crashlog.threads[thread_idx] 657 return 658 thread_insts_match = self.thread_instrs_regex.search(line) 659 if thread_insts_match: 660 self.parse_mode = CrashLogParseMode.INSTRS 661 return 662 thread_match = self.thread_regex.search(line) 663 if thread_match: 664 self.app_specific_backtrace = False 665 self.parse_mode = CrashLogParseMode.THREAD 666 thread_idx = int(thread_match.group(1)) 667 self.thread = self.crashlog.Thread(thread_idx, False) 668 return 669 return 670 elif line.startswith('Binary Images:'): 671 self.parse_mode = CrashLogParseMode.IMAGES 672 return 673 elif line.startswith('Application Specific Backtrace'): 674 app_backtrace_match = self.app_backtrace_regex.search(line) 675 if app_backtrace_match: 676 self.parse_mode = CrashLogParseMode.THREAD 677 self.app_specific_backtrace = True 678 idx = int(app_backtrace_match.group(1)) 679 self.thread = self.crashlog.Thread(idx, True) 680 elif line.startswith('Last Exception Backtrace:'): # iOS 681 self.parse_mode = CrashLogParseMode.THREAD 682 self.app_specific_backtrace = True 683 idx = 1 684 self.thread = self.crashlog.Thread(idx, True) 685 self.crashlog.info_lines.append(line.strip()) 686 687 def parse_thread(self, line): 688 if line.startswith('Thread'): 689 return 690 if self.null_frame_regex.search(line): 691 print('warning: thread parser ignored null-frame: "%s"' % line) 692 return 693 frame_match = self.frame_regex.search(line) 694 if frame_match: 695 (frame_id, frame_img_name, _, frame_img_version, _, 696 frame_addr, frame_ofs) = frame_match.groups() 697 ident = frame_img_name 698 self.thread.add_ident(ident) 699 if ident not in self.crashlog.idents: 700 self.crashlog.idents.append(ident) 701 self.thread.frames.append(self.crashlog.Frame(int(frame_id), int( 702 frame_addr, 0), frame_ofs)) 703 else: 704 print('error: frame regex failed for line: "%s"' % line) 705 706 def parse_images(self, line): 707 image_match = self.image_regex_uuid.search(line) 708 if image_match: 709 (img_lo, img_hi, img_name, _, img_version, _, 710 _, img_uuid, img_path) = image_match.groups() 711 image = self.crashlog.DarwinImage(int(img_lo, 0), int(img_hi, 0), 712 img_name.strip(), 713 img_version.strip() 714 if img_version else "", 715 uuid.UUID(img_uuid), img_path, 716 self.verbose) 717 self.crashlog.images.append(image) 718 else: 719 print("error: image regex failed for: %s" % line) 720 721 722 def parse_thread_registers(self, line): 723 stripped_line = line.strip() 724 # "r12: 0x00007fff6b5939c8 r13: 0x0000000007000006 r14: 0x0000000000002a03 r15: 0x0000000000000c00" 725 reg_values = re.findall( 726 '([a-zA-Z0-9]+: 0[Xx][0-9a-fA-F]+) *', stripped_line) 727 for reg_value in reg_values: 728 (reg, value) = reg_value.split(': ') 729 self.thread.registers[reg.strip()] = int(value, 0) 730 731 def parse_system(self, line): 732 self.crashlog.system_profile.append(line) 733 734 def parse_instructions(self, line): 735 pass 736 737 738def usage(): 739 print("Usage: lldb-symbolicate.py [-n name] executable-image") 740 sys.exit(0) 741 742 743class Interactive(cmd.Cmd): 744 '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.''' 745 image_option_parser = None 746 747 def __init__(self, crash_logs): 748 cmd.Cmd.__init__(self) 749 self.use_rawinput = False 750 self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.' 751 self.crash_logs = crash_logs 752 self.prompt = '% ' 753 754 def default(self, line): 755 '''Catch all for unknown command, which will exit the interpreter.''' 756 print("uknown command: %s" % line) 757 return True 758 759 def do_q(self, line): 760 '''Quit command''' 761 return True 762 763 def do_quit(self, line): 764 '''Quit command''' 765 return True 766 767 def do_symbolicate(self, line): 768 description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information, 769 inlined stack frames back to the concrete functions, and disassemble the location of the crash 770 for the first frame of the crashed thread.''' 771 option_parser = CreateSymbolicateCrashLogOptions( 772 'symbolicate', description, False) 773 command_args = shlex.split(line) 774 try: 775 (options, args) = option_parser.parse_args(command_args) 776 except: 777 return 778 779 if args: 780 # We have arguments, they must valid be crash log file indexes 781 for idx_str in args: 782 idx = int(idx_str) 783 if idx < len(self.crash_logs): 784 SymbolicateCrashLog(self.crash_logs[idx], options) 785 else: 786 print('error: crash log index %u is out of range' % (idx)) 787 else: 788 # No arguments, symbolicate all crash logs using the options 789 # provided 790 for idx in range(len(self.crash_logs)): 791 SymbolicateCrashLog(self.crash_logs[idx], options) 792 793 def do_list(self, line=None): 794 '''Dump a list of all crash logs that are currently loaded. 795 796 USAGE: list''' 797 print('%u crash logs are loaded:' % len(self.crash_logs)) 798 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 799 print('[%u] = %s' % (crash_log_idx, crash_log.path)) 800 801 def do_image(self, line): 802 '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.''' 803 usage = "usage: %prog [options] <PATH> [PATH ...]" 804 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.''' 805 command_args = shlex.split(line) 806 if not self.image_option_parser: 807 self.image_option_parser = optparse.OptionParser( 808 description=description, prog='image', usage=usage) 809 self.image_option_parser.add_option( 810 '-a', 811 '--all', 812 action='store_true', 813 help='show all images', 814 default=False) 815 try: 816 (options, args) = self.image_option_parser.parse_args(command_args) 817 except: 818 return 819 820 if args: 821 for image_path in args: 822 fullpath_search = image_path[0] == '/' 823 for (crash_log_idx, crash_log) in enumerate(self.crash_logs): 824 matches_found = 0 825 for (image_idx, image) in enumerate(crash_log.images): 826 if fullpath_search: 827 if image.get_resolved_path() == image_path: 828 matches_found += 1 829 print('[%u] ' % (crash_log_idx), image) 830 else: 831 image_basename = image.get_resolved_path_basename() 832 if image_basename == image_path: 833 matches_found += 1 834 print('[%u] ' % (crash_log_idx), image) 835 if matches_found == 0: 836 for (image_idx, image) in enumerate(crash_log.images): 837 resolved_image_path = image.get_resolved_path() 838 if resolved_image_path and string.find( 839 image.get_resolved_path(), image_path) >= 0: 840 print('[%u] ' % (crash_log_idx), image) 841 else: 842 for crash_log in self.crash_logs: 843 for (image_idx, image) in enumerate(crash_log.images): 844 print('[%u] %s' % (image_idx, image)) 845 return False 846 847 848def interactive_crashlogs(debugger, options, args): 849 crash_log_files = list() 850 for arg in args: 851 for resolved_path in glob.glob(arg): 852 crash_log_files.append(resolved_path) 853 854 crash_logs = list() 855 for crash_log_file in crash_log_files: 856 try: 857 crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) 858 except Exception as e: 859 print(e) 860 continue 861 if options.debug: 862 crash_log.dump() 863 if not crash_log.images: 864 print('error: no images in crash log "%s"' % (crash_log)) 865 continue 866 else: 867 crash_logs.append(crash_log) 868 869 interpreter = Interactive(crash_logs) 870 # List all crash logs that were imported 871 interpreter.do_list() 872 interpreter.cmdloop() 873 874 875def save_crashlog(debugger, command, exe_ctx, result, dict): 876 usage = "usage: %prog [options] <output-path>" 877 description = '''Export the state of current target into a crashlog file''' 878 parser = optparse.OptionParser( 879 description=description, 880 prog='save_crashlog', 881 usage=usage) 882 parser.add_option( 883 '-v', 884 '--verbose', 885 action='store_true', 886 dest='verbose', 887 help='display verbose debug info', 888 default=False) 889 try: 890 (options, args) = parser.parse_args(shlex.split(command)) 891 except: 892 result.PutCString("error: invalid options") 893 return 894 if len(args) != 1: 895 result.PutCString( 896 "error: invalid arguments, a single output file is the only valid argument") 897 return 898 out_file = open(args[0], 'w') 899 if not out_file: 900 result.PutCString( 901 "error: failed to open file '%s' for writing...", 902 args[0]) 903 return 904 target = exe_ctx.target 905 if target: 906 identifier = target.executable.basename 907 process = exe_ctx.process 908 if process: 909 pid = process.id 910 if pid != lldb.LLDB_INVALID_PROCESS_ID: 911 out_file.write( 912 'Process: %s [%u]\n' % 913 (identifier, pid)) 914 out_file.write('Path: %s\n' % (target.executable.fullpath)) 915 out_file.write('Identifier: %s\n' % (identifier)) 916 out_file.write('\nDate/Time: %s\n' % 917 (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) 918 out_file.write( 919 'OS Version: Mac OS X %s (%s)\n' % 920 (platform.mac_ver()[0], subprocess.check_output('sysctl -n kern.osversion', shell=True).decode("utf-8"))) 921 out_file.write('Report Version: 9\n') 922 for thread_idx in range(process.num_threads): 923 thread = process.thread[thread_idx] 924 out_file.write('\nThread %u:\n' % (thread_idx)) 925 for (frame_idx, frame) in enumerate(thread.frames): 926 frame_pc = frame.pc 927 frame_offset = 0 928 if frame.function: 929 block = frame.GetFrameBlock() 930 block_range = block.range[frame.addr] 931 if block_range: 932 block_start_addr = block_range[0] 933 frame_offset = frame_pc - block_start_addr.GetLoadAddress(target) 934 else: 935 frame_offset = frame_pc - frame.function.addr.GetLoadAddress(target) 936 elif frame.symbol: 937 frame_offset = frame_pc - frame.symbol.addr.GetLoadAddress(target) 938 out_file.write( 939 '%-3u %-32s 0x%16.16x %s' % 940 (frame_idx, frame.module.file.basename, frame_pc, frame.name)) 941 if frame_offset > 0: 942 out_file.write(' + %u' % (frame_offset)) 943 line_entry = frame.line_entry 944 if line_entry: 945 if options.verbose: 946 # This will output the fullpath + line + column 947 out_file.write(' %s' % (line_entry)) 948 else: 949 out_file.write( 950 ' %s:%u' % 951 (line_entry.file.basename, line_entry.line)) 952 column = line_entry.column 953 if column: 954 out_file.write(':%u' % (column)) 955 out_file.write('\n') 956 957 out_file.write('\nBinary Images:\n') 958 for module in target.modules: 959 text_segment = module.section['__TEXT'] 960 if text_segment: 961 text_segment_load_addr = text_segment.GetLoadAddress(target) 962 if text_segment_load_addr != lldb.LLDB_INVALID_ADDRESS: 963 text_segment_end_load_addr = text_segment_load_addr + text_segment.size 964 identifier = module.file.basename 965 module_version = '???' 966 module_version_array = module.GetVersion() 967 if module_version_array: 968 module_version = '.'.join( 969 map(str, module_version_array)) 970 out_file.write( 971 ' 0x%16.16x - 0x%16.16x %s (%s - ???) <%s> %s\n' % 972 (text_segment_load_addr, 973 text_segment_end_load_addr, 974 identifier, 975 module_version, 976 module.GetUUIDString(), 977 module.file.fullpath)) 978 out_file.close() 979 else: 980 result.PutCString("error: invalid target") 981 982 983def Symbolicate(debugger, command, result, dict): 984 try: 985 SymbolicateCrashLogs(debugger, shlex.split(command)) 986 except Exception as e: 987 result.PutCString("error: python exception: %s" % e) 988 989 990def SymbolicateCrashLog(crash_log, options): 991 if options.debug: 992 crash_log.dump() 993 if not crash_log.images: 994 print('error: no images in crash log') 995 return 996 997 if options.dump_image_list: 998 print("Binary Images:") 999 for image in crash_log.images: 1000 if options.verbose: 1001 print(image.debug_dump()) 1002 else: 1003 print(image) 1004 1005 target = crash_log.create_target() 1006 if not target: 1007 return 1008 exe_module = target.GetModuleAtIndex(0) 1009 images_to_load = list() 1010 loaded_images = list() 1011 if options.load_all_images: 1012 # --load-all option was specified, load everything up 1013 for image in crash_log.images: 1014 images_to_load.append(image) 1015 else: 1016 # Only load the images found in stack frames for the crashed threads 1017 if options.crashed_only: 1018 for thread in crash_log.threads: 1019 if thread.did_crash(): 1020 for ident in thread.idents: 1021 images = crash_log.find_images_with_identifier(ident) 1022 if images: 1023 for image in images: 1024 images_to_load.append(image) 1025 else: 1026 print('error: can\'t find image for identifier "%s"' % ident) 1027 else: 1028 for ident in crash_log.idents: 1029 images = crash_log.find_images_with_identifier(ident) 1030 if images: 1031 for image in images: 1032 images_to_load.append(image) 1033 else: 1034 print('error: can\'t find image for identifier "%s"' % ident) 1035 1036 for image in images_to_load: 1037 if image not in loaded_images: 1038 err = image.add_module(target) 1039 if err: 1040 print(err) 1041 else: 1042 loaded_images.append(image) 1043 1044 if crash_log.backtraces: 1045 for thread in crash_log.backtraces: 1046 thread.dump_symbolicated(crash_log, options) 1047 print() 1048 1049 for thread in crash_log.threads: 1050 thread.dump_symbolicated(crash_log, options) 1051 print() 1052 1053 1054def CreateSymbolicateCrashLogOptions( 1055 command_name, 1056 description, 1057 add_interactive_options): 1058 usage = "usage: %prog [options] <FILE> [FILE ...]" 1059 option_parser = optparse.OptionParser( 1060 description=description, prog='crashlog', usage=usage) 1061 option_parser.add_option( 1062 '--verbose', 1063 '-v', 1064 action='store_true', 1065 dest='verbose', 1066 help='display verbose debug info', 1067 default=False) 1068 option_parser.add_option( 1069 '--debug', 1070 '-g', 1071 action='store_true', 1072 dest='debug', 1073 help='display verbose debug logging', 1074 default=False) 1075 option_parser.add_option( 1076 '--load-all', 1077 '-a', 1078 action='store_true', 1079 dest='load_all_images', 1080 help='load all executable images, not just the images found in the crashed stack frames', 1081 default=False) 1082 option_parser.add_option( 1083 '--images', 1084 action='store_true', 1085 dest='dump_image_list', 1086 help='show image list', 1087 default=False) 1088 option_parser.add_option( 1089 '--debug-delay', 1090 type='int', 1091 dest='debug_delay', 1092 metavar='NSEC', 1093 help='pause for NSEC seconds for debugger', 1094 default=0) 1095 option_parser.add_option( 1096 '--crashed-only', 1097 '-c', 1098 action='store_true', 1099 dest='crashed_only', 1100 help='only symbolicate the crashed thread', 1101 default=False) 1102 option_parser.add_option( 1103 '--disasm-depth', 1104 '-d', 1105 type='int', 1106 dest='disassemble_depth', 1107 help='set the depth in stack frames that should be disassembled (default is 1)', 1108 default=1) 1109 option_parser.add_option( 1110 '--disasm-all', 1111 '-D', 1112 action='store_true', 1113 dest='disassemble_all_threads', 1114 help='enabled disassembly of frames on all threads (not just the crashed thread)', 1115 default=False) 1116 option_parser.add_option( 1117 '--disasm-before', 1118 '-B', 1119 type='int', 1120 dest='disassemble_before', 1121 help='the number of instructions to disassemble before the frame PC', 1122 default=4) 1123 option_parser.add_option( 1124 '--disasm-after', 1125 '-A', 1126 type='int', 1127 dest='disassemble_after', 1128 help='the number of instructions to disassemble after the frame PC', 1129 default=4) 1130 option_parser.add_option( 1131 '--source-context', 1132 '-C', 1133 type='int', 1134 metavar='NLINES', 1135 dest='source_context', 1136 help='show NLINES source lines of source context (default = 4)', 1137 default=4) 1138 option_parser.add_option( 1139 '--source-frames', 1140 type='int', 1141 metavar='NFRAMES', 1142 dest='source_frames', 1143 help='show source for NFRAMES (default = 4)', 1144 default=4) 1145 option_parser.add_option( 1146 '--source-all', 1147 action='store_true', 1148 dest='source_all', 1149 help='show source for all threads, not just the crashed thread', 1150 default=False) 1151 if add_interactive_options: 1152 option_parser.add_option( 1153 '-i', 1154 '--interactive', 1155 action='store_true', 1156 help='parse all crash logs and enter interactive mode', 1157 default=False) 1158 return option_parser 1159 1160 1161def SymbolicateCrashLogs(debugger, command_args): 1162 description = '''Symbolicate one or more darwin crash log files to provide source file and line information, 1163inlined stack frames back to the concrete functions, and disassemble the location of the crash 1164for the first frame of the crashed thread. 1165If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter 1166for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been 1167created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows 1168you to explore the program as if it were stopped at the locations described in the crash log and functions can 1169be disassembled and lookups can be performed using the addresses found in the crash log.''' 1170 option_parser = CreateSymbolicateCrashLogOptions( 1171 'crashlog', description, True) 1172 try: 1173 (options, args) = option_parser.parse_args(command_args) 1174 except: 1175 return 1176 1177 if options.debug: 1178 print('command_args = %s' % command_args) 1179 print('options', options) 1180 print('args', args) 1181 1182 if options.debug_delay > 0: 1183 print("Waiting %u seconds for debugger to attach..." % options.debug_delay) 1184 time.sleep(options.debug_delay) 1185 error = lldb.SBError() 1186 1187 if args: 1188 if options.interactive: 1189 interactive_crashlogs(debugger, options, args) 1190 else: 1191 for crash_log_file in args: 1192 crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) 1193 SymbolicateCrashLog(crash_log, options) 1194if __name__ == '__main__': 1195 # Create a new debugger instance 1196 debugger = lldb.SBDebugger.Create() 1197 SymbolicateCrashLogs(debugger, sys.argv[1:]) 1198 lldb.SBDebugger.Destroy(debugger) 1199elif getattr(lldb, 'debugger', None): 1200 lldb.debugger.HandleCommand( 1201 'command script add -f lldb.macosx.crashlog.Symbolicate crashlog') 1202 lldb.debugger.HandleCommand( 1203 'command script add -f lldb.macosx.crashlog.save_crashlog save_crashlog') 1204