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