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