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