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