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