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