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