diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt --- a/lldb/bindings/python/CMakeLists.txt +++ b/lldb/bindings/python/CMakeLists.txt @@ -114,6 +114,7 @@ ${swig_target} ${lldb_python_target_dir} "macosx" FILES "${LLDB_SOURCE_DIR}/examples/python/crashlog.py" + "${LLDB_SOURCE_DIR}/examples/python/scripted_process/crashlog_scripted_process.py" "${LLDB_SOURCE_DIR}/examples/darwin/heap_find/heap.py") create_python_package( diff --git a/lldb/examples/python/crashlog.py b/lldb/examples/python/crashlog.py --- a/lldb/examples/python/crashlog.py +++ b/lldb/examples/python/crashlog.py @@ -63,7 +63,6 @@ from lldb.utils import symbolication - def read_plist(s): if sys.version_info.major == 3: return plistlib.loads(s) @@ -780,138 +779,6 @@ sys.exit(0) -class Interactive(cmd.Cmd): - '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.''' - image_option_parser = None - - def __init__(self, crash_logs): - cmd.Cmd.__init__(self) - self.use_rawinput = False - self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.' - self.crash_logs = crash_logs - self.prompt = '% ' - - def default(self, line): - '''Catch all for unknown command, which will exit the interpreter.''' - print("uknown command: %s" % line) - return True - - def do_q(self, line): - '''Quit command''' - return True - - def do_quit(self, line): - '''Quit command''' - return True - - def do_symbolicate(self, line): - description = '''Symbolicate one or more darwin crash log files by index to provide source file and line information, - inlined stack frames back to the concrete functions, and disassemble the location of the crash - for the first frame of the crashed thread.''' - option_parser = CreateSymbolicateCrashLogOptions( - 'symbolicate', description, False) - command_args = shlex.split(line) - try: - (options, args) = option_parser.parse_args(command_args) - except: - return - - if args: - # We have arguments, they must valid be crash log file indexes - for idx_str in args: - idx = int(idx_str) - if idx < len(self.crash_logs): - SymbolicateCrashLog(self.crash_logs[idx], options) - else: - print('error: crash log index %u is out of range' % (idx)) - else: - # No arguments, symbolicate all crash logs using the options - # provided - for idx in range(len(self.crash_logs)): - SymbolicateCrashLog(self.crash_logs[idx], options) - - def do_list(self, line=None): - '''Dump a list of all crash logs that are currently loaded. - - USAGE: list''' - print('%u crash logs are loaded:' % len(self.crash_logs)) - for (crash_log_idx, crash_log) in enumerate(self.crash_logs): - print('[%u] = %s' % (crash_log_idx, crash_log.path)) - - def do_image(self, line): - '''Dump information about one or more binary images in the crash log given an image basename, or all images if no arguments are provided.''' - usage = "usage: %prog [options] [PATH ...]" - description = '''Dump information about one or more images in all crash logs. The can be a full path, image basename, or partial path. Searches are done in this order.''' - command_args = shlex.split(line) - if not self.image_option_parser: - self.image_option_parser = optparse.OptionParser( - description=description, prog='image', usage=usage) - self.image_option_parser.add_option( - '-a', - '--all', - action='store_true', - help='show all images', - default=False) - try: - (options, args) = self.image_option_parser.parse_args(command_args) - except: - return - - if args: - for image_path in args: - fullpath_search = image_path[0] == '/' - for (crash_log_idx, crash_log) in enumerate(self.crash_logs): - matches_found = 0 - for (image_idx, image) in enumerate(crash_log.images): - if fullpath_search: - if image.get_resolved_path() == image_path: - matches_found += 1 - print('[%u] ' % (crash_log_idx), image) - else: - image_basename = image.get_resolved_path_basename() - if image_basename == image_path: - matches_found += 1 - print('[%u] ' % (crash_log_idx), image) - if matches_found == 0: - for (image_idx, image) in enumerate(crash_log.images): - resolved_image_path = image.get_resolved_path() - if resolved_image_path and string.find( - image.get_resolved_path(), image_path) >= 0: - print('[%u] ' % (crash_log_idx), image) - else: - for crash_log in self.crash_logs: - for (image_idx, image) in enumerate(crash_log.images): - print('[%u] %s' % (image_idx, image)) - return False - - -def interactive_crashlogs(debugger, options, args): - crash_log_files = list() - for arg in args: - for resolved_path in glob.glob(arg): - crash_log_files.append(resolved_path) - - crash_logs = list() - for crash_log_file in crash_log_files: - try: - crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) - except Exception as e: - print(e) - continue - if options.debug: - crash_log.dump() - if not crash_log.images: - print('error: no images in crash log "%s"' % (crash_log)) - continue - else: - crash_logs.append(crash_log) - - interpreter = Interactive(crash_logs) - # List all crash logs that were imported - interpreter.do_list() - interpreter.cmdloop() - - def save_crashlog(debugger, command, exe_ctx, result, dict): usage = "usage: %prog [options] " description = '''Export the state of current target into a crashlog file''' @@ -1106,6 +973,43 @@ for error in crash_log.errors: print(error) +def load_crashlog_in_scripted_process(debugger, crash_log_file): + result = lldb.SBCommandReturnObject() + + crashlog_path = os.path.expanduser(crash_log_file) + if not os.path.exists(crashlog_path): + result.PutCString("error: crashlog file %s does not exist" % crashlog_path) + + try: + crashlog = CrashLogParser().parse(debugger, crashlog_path, False) + except Exception as e: + result.PutCString("error: python exception: %s" % e) + return + + target = crashlog.create_target() + if not target: + result.PutCString("error: couldn't create target") + return + + ci = debugger.GetCommandInterpreter() + if not ci: + result.PutCString("error: couldn't get command interpreter") + return + + res = lldb.SBCommandReturnObject() + ci.HandleCommand('script from lldb.macosx import crashlog_scripted_process', res) + if not res.Succeeded(): + result.PutCString("error: couldn't import crashlog scripted process module") + return + + structured_data = lldb.SBStructuredData() + structured_data.SetFromJSON(json.dumps({ "crashlog_path" : crashlog_path })) + launch_info = lldb.SBLaunchInfo(None) + launch_info.SetProcessPluginName("ScriptedProcess") + launch_info.SetScriptedProcessClassName("crashlog_scripted_process.CrashLogScriptedProcess") + launch_info.SetScriptedProcessDictionary(structured_data) + error = lldb.SBError() + process = target.Launch(launch_info, error) def CreateSymbolicateCrashLogOptions( command_name, @@ -1209,8 +1113,14 @@ '-i', '--interactive', action='store_true', - help='parse all crash logs and enter interactive mode', + help='parse a crash log and load it in a ScriptedProcess', default=False) + option_parser.add_option( + '-b', + '--batch', + action='store_true', + help='dump symbolicated stackframes without creating a debug session', + default=True) return option_parser @@ -1242,11 +1152,23 @@ time.sleep(options.debug_delay) error = lldb.SBError() - if args: + def should_run_in_interactive_mode(options, ci): if options.interactive: - interactive_crashlogs(debugger, options, args) + return True + elif options.batch: + return False + # elif ci and ci.IsInteractive(): + # return True else: - for crash_log_file in args: + return False + + ci = debugger.GetCommandInterpreter() + + if args: + for crash_log_file in args: + if should_run_in_interactive_mode(options, ci): + load_crashlog_in_scripted_process(debugger, crash_log_file) + else: crash_log = CrashLogParser().parse(debugger, crash_log_file, options.verbose) SymbolicateCrashLog(crash_log, options) diff --git a/lldb/examples/python/scripted_process/crashlog_scripted_process.py b/lldb/examples/python/scripted_process/crashlog_scripted_process.py new file mode 100644 --- /dev/null +++ b/lldb/examples/python/scripted_process/crashlog_scripted_process.py @@ -0,0 +1,148 @@ +import os,json,struct,signal + +from typing import Any, Dict + +import lldb +from lldb.plugins.scripted_process import ScriptedProcess +from lldb.plugins.scripted_process import ScriptedThread + +from lldb.macosx.crashlog import CrashLog,CrashLogParser + +class CrashLogScriptedProcess(ScriptedProcess): + def parse_crashlog(self): + try: + crash_log = CrashLogParser().parse(self.dbg, self.crashlog_path, False) + except Exception as e: + return + + self.pid = crash_log.process_id + self.crashed_thread_idx = crash_log.crashed_thread_idx + self.loaded_images = [] + + for thread in crash_log.threads: + if thread.did_crash(): + for ident in thread.idents: + images = crash_log.find_images_with_identifier(ident) + if images: + for image in images: + #TODO: Add to self.loaded_images and load images in lldb + err = image.add_module(self.target) + if err: + print(err) + else: + self.loaded_images.append(image) + self.threads[thread.index] = CrashLogScriptedThread(self, None, thread) + + def __init__(self, target: lldb.SBTarget, args : lldb.SBStructuredData): + super().__init__(target, args) + + if not self.target or not self.target.IsValid(): + return + + self.crashlog_path = None + + crashlog_path = args.GetValueForKey("crashlog_path") + if crashlog_path and crashlog_path.IsValid(): + if crashlog_path.GetType() == lldb.eStructuredDataTypeString: + self.crashlog_path = crashlog_path.GetStringValue(4096) + + if not self.crashlog_path: + return + + self.pid = super().get_process_id() + self.crashed_thread_idx = 0 + self.parse_crashlog() + + def get_memory_region_containing_address(self, addr: int) -> lldb.SBMemoryRegionInfo: + return None + + def get_thread_with_id(self, tid: int): + return {} + + def get_registers_for_thread(self, tid: int): + return {} + + def read_memory_at_address(self, addr: int, size: int) -> lldb.SBData: + # NOTE: CrashLogs don't contain any memory. + return lldb.SBData() + + def get_loaded_images(self): + # TODO: Iterate over corefile_target modules and build a data structure + # from it. + return self.loaded_images + + def get_process_id(self) -> int: + return self.pid + + def should_stop(self) -> bool: + return True + + def is_alive(self) -> bool: + return True + + def get_scripted_thread_plugin(self): + return CrashLogScriptedThread.__module__ + "." + CrashLogScriptedThread.__name__ + +class CrashLogScriptedThread(ScriptedThread): + def create_register_ctx(self): + if not self.has_crashed: + return dict.fromkeys([*map(lambda reg: reg['name'], self.register_info['registers'])] , 0) + + if not self.backing_thread or not len(self.backing_thread.registers): + return dict.fromkeys([*map(lambda reg: reg['name'], self.register_info['registers'])] , 0) + + for reg in self.register_info['registers']: + reg_name = reg['name'] + if reg_name in self.backing_thread.registers: + self.register_ctx[reg_name] = self.backing_thread.registers[reg_name] + else: + self.register_ctx[reg_name] = 0 + + return self.register_ctx + + def create_stackframes(self): + if not self.has_crashed: + return None + + if not self.backing_thread or not len(self.backing_thread.frames): + return None + + for frame in self.backing_thread.frames: + sym_addr = lldb.SBAddress() + sym_addr.SetLoadAddress(frame.pc, self.target) + if not sym_addr.IsValid(): + continue + self.frames.append({"idx": frame.index, "pc": frame.pc}) + + return self.frames + + def __init__(self, process, args, crashlog_thread): + super().__init__(process, args) + + self.backing_thread = crashlog_thread + self.idx = self.backing_thread.index + self.has_crashed = (self.scripted_process.crashed_thread_idx == self.idx) + self.create_stackframes() + + def get_thread_id(self) -> int: + return self.idx + + def get_name(self) -> str: + return CrashLogScriptedThread.__name__ + ".thread-" + str(self.idx) + + def get_state(self): + if not self.has_crashed: + return lldb.eStateStopped + return lldb.eStateCrashed + + def get_stop_reason(self) -> Dict[str, Any]: + if not self.has_crashed: + return { "type": lldb.eStopReasonNone, "data": { }} + # TODO: Investigate what stop reason should be reported when crashed + return { "type": lldb.eStopReasonException, "data": { "desc": "EXC_BAD_ACCESS" }} + + def get_register_context(self) -> str: + if not self.register_ctx: + self.register_ctx = self.create_register_ctx() + + return struct.pack("{}Q".format(len(self.register_ctx)), *self.register_ctx.values()) diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp --- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp @@ -303,6 +303,9 @@ StructuredData::DictionarySP thread_info_sp = GetInterface().GetThreadsInfo(); + // FIXME: Need to sort the dictionary otherwise the thread ids won't match the + // thread indices. + if (!thread_info_sp) return ScriptedInterface::ErrorWithMessage( LLVM_PRETTY_FUNCTION, diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/scripted_crashlog.ips b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/scripted_crashlog.ips new file mode 100644 --- /dev/null +++ b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/scripted_crashlog.ips @@ -0,0 +1,75 @@ +{"app_name":"scripted_crashlog_json.test.tmp.out","timestamp":"2022-02-14 16:30:31.00 -0800","app_version":"","slice_uuid":"b928ee77-9429-334f-ac88-41440bb3d4c7","build_version":"","platform":1,"share_with_app_devs":0,"is_first_party":1,"bug_type":"309","os_version":"macOS 12.3","incident_id":"E57CADE7-DC44-45CE-8D16-18EBC4406B97","name":"scripted_crashlog_json.test.tmp.out"} +{ + "uptime" : 260000, + "procLaunch" : "2022-02-14 16:30:31.8048 -0800", + "procRole" : "Unspecified", + "version" : 2, + "userID" : 501, + "deployVersion" : 210, + "modelCode" : "MacBookPro18,2", + "procStartAbsTime" : 6478056069413, + "coalitionID" : 22196, + "osVersion" : { + "train" : "macOS 12.3", + "build" : "", + "releaseType" : "" + }, + "captureTime" : "2022-02-14 16:30:31.8096 -0800", + "incident" : "E57CADE7-DC44-45CE-8D16-18EBC4406B97", + "bug_type" : "309", + "pid" : 92190, + "procExitAbsTime" : 6478056175721, + "translated" : false, + "cpuType" : "ARM-64", + "procName" : "scripted_crashlog_json.test.tmp.out", + "procPath" : "\/Users\/USER\/*\/scripted_crashlog_json.test.tmp.out", + "parentProc" : "zsh", + "parentPid" : 82132, + "coalitionName" : "com.apple.Terminal", + "crashReporterKey" : "CDC11418-EDBF-2A49-0D83-8B441A5004B0", + "responsiblePid" : 76395, + "responsibleProc" : "Terminal", + "wakeTime" : 14889, + "sleepWakeUUID" : "BCA947AE-2F0A-44C7-8445-FEDFFA236CD0", + "sip" : "enabled", + "vmRegionInfo" : "0 is not in any region. Bytes before following region: 4372692992\n REGION TYPE START - END [ VSIZE] PRT\/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 104a20000-104a24000 [ 16K] r-x\/r-x SM=COW ....test.tmp.out", + "isCorpse" : 1, + "exception" : {"codes":"0x0000000000000001, 0x0000000000000000","rawCodes":[1,0],"type":"EXC_BAD_ACCESS","signal":"SIGSEGV","subtype":"KERN_INVALID_ADDRESS at 0x0000000000000000"}, + "termination" : {"flags":0,"code":11,"namespace":"SIGNAL","indicator":"Segmentation fault: 11","byProc":"exc handler","byPid":92190}, + "vmregioninfo" : "0 is not in any region. Bytes before following region: 4372692992\n REGION TYPE START - END [ VSIZE] PRT\/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 104a20000-104a24000 [ 16K] r-x\/r-x SM=COW ....test.tmp.out", + "extMods" : {"caller":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"system":{"thread_create":0,"thread_set_state":156,"task_for_pid":28},"targeted":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"warnings":0}, + "faultingThread" : 0, + "threads" : [{"triggered":true,"id":4567339,"threadState":{"x":[{"value":1},{"value":6094187136},{"value":6094187152},{"value":6094187720},{"value":0},{"value":0},{"value":0},{"value":0},{"value":1},{"value":0},{"value":0},{"value":2},{"value":2},{"value":0},{"value":80},{"value":0},{"value":13118353544},{"value":7701436843874442528},{"value":0},{"value":4373676128},{"sourceLine":8,"value":4372709256,"sourceFile":"test.c","symbol":"main","symbolLocation":0},{"value":4373332080,"symbolLocation":0,"symbol":"dyld4::sConfigBuffer"},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0},{"value":0}],"flavor":"ARM_THREAD_STATE64","lr":{"value":4372709248},"cpsr":{"value":1610616832},"fp":{"value":6094186736},"sp":{"value":6094186720},"esr":{"value":2449473606,"description":"(Data Abort) byte write Translation fault"},"pc":{"value":4372709224,"matchesCrashFrame":1},"far":{"value":0}},"queue":"com.apple.main-thread","frames":[{"imageOffset":16232,"sourceLine":3,"sourceFile":"test.c","symbol":"foo","imageIndex":0,"symbolLocation":16},{"imageOffset":16256,"sourceLine":6,"sourceFile":"test.c","symbol":"bar","imageIndex":0,"symbolLocation":12},{"imageOffset":16288,"sourceLine":8,"sourceFile":"test.c","symbol":"main","imageIndex":0,"symbolLocation":24},{"imageOffset":20620,"symbol":"start","symbolLocation":520,"imageIndex":1}]}], + "usedImages" : [ + { + "source" : "P", + "arch" : "arm64", + "base" : 4372692992, + "size" : 16384, + "uuid" : "b928ee77-9429-334f-ac88-41440bb3d4c7", + "path" : "\/Users\/USER\/*\/scripted_crashlog_json.test.tmp.out", + "name" : "scripted_crashlog_json.test.tmp.out" + }, + { + "source" : "P", + "arch" : "arm64e", + "base" : 4372938752, + "size" : 393216, + "uuid" : "41293cda-474b-3700-924e-6ba0f7698eac", + "path" : "\/usr\/lib\/dyld", + "name" : "dyld" + } +], + "sharedCache" : { + "base" : 6924156928, + "size" : 3151052800, + "uuid" : "2ff78c31-e522-3e4a-a414-568e926f7274" +}, + "vmSummary" : "ReadOnly portion of Libraries: Total=589.5M resident=0K(0%) swapped_out_or_unallocated=589.5M(100%)\nWritable regions: Total=529.1M written=0K(0%) resident=0K(0%) swapped_out=0K(0%) unallocated=529.1M(100%)\n\n VIRTUAL REGION \nREGION TYPE SIZE COUNT (non-coalesced) \n=========== ======= ======= \nKernel Alloc Once 32K 1 \nMALLOC 137.2M 11 \nMALLOC guard page 96K 5 \nMALLOC_NANO (reserved) 384.0M 1 reserved VM address space (unallocated)\nSTACK GUARD 56.0M 1 \nStack 8176K 1 \n__AUTH 46K 11 \n__AUTH_CONST 67K 38 \n__DATA 173K 36 \n__DATA_CONST 242K 39 \n__DATA_DIRTY 73K 21 \n__LINKEDIT 584.9M 3 \n__OBJC_CONST 10K 5 \n__OBJC_RO 82.9M 1 \n__OBJC_RW 3168K 1 \n__TEXT 4696K 43 \ndyld private memory 1024K 1 \nshared memory 48K 2 \n=========== ======= ======= \nTOTAL 1.2G 221 \nTOTAL, minus reserved VM space 878.5M 221 \n", + "legacyInfo" : { + "threadTriggered" : { + "queue" : "com.apple.main-thread" + } +}, + "trialInfo" : { } +} diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/scripted_crashlog_json.test b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/scripted_crashlog_json.test new file mode 100644 --- /dev/null +++ b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/scripted_crashlog_json.test @@ -0,0 +1,10 @@ +# RUN: %clang_host -g %S/Inputs/test.c -o %t.out + +# RUN: cp %S/Inputs/scripted_crashlog.ips %t.crash +# RUN: %python %S/patch-crashlog.py --binary %t.out --crashlog %t.crash --offsets '{"main":20, "bar":9, "foo":16}' --json +# RUN: %lldb %t.out -o 'command script import lldb.macosx.crashlog' -o 'crashlog -i %t.crash' -o 'process status' 2>&1 | FileCheck %s + +# CHECK: "crashlog" {{.*}} commands have been installed, use the "--help" options on these commands +# CHECK: Process 92190 stopped +# CHECK: * thread #1, name = 'CrashLogScriptedThread.thread-0', stop reason = EXC_BAD_ACCESS +# CHECK: frame #0: 0x0000000104a23f68 scripted_crashlog_json.test.tmp.out`foo at test.c:3:6 [artificial]