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