1# This script allows to use LLDB in a way similar to GDB's batch mode. That is, given a text file 2# containing LLDB commands (one command per line), this script will execute the commands one after 3# the other. 4# LLDB also has the -s and -S commandline options which also execute a list of commands from a text 5# file. However, this command are execute `immediately`: the command of a `run` or `continue` 6# command will be executed immediately after the `run` or `continue`, without waiting for the next 7# breakpoint to be hit. This a command sequence like the following will not yield reliable results: 8# 9# break 11 10# run 11# print x 12# 13# Most of the time the `print` command will be executed while the program is still running will thus 14# fail. Using this Python script, the above will work as expected. 15 16from __future__ import print_function 17import lldb 18import os 19import sys 20import threading 21import re 22import time 23 24try: 25 import thread 26except ModuleNotFoundError: 27 # The `thread` module was renamed to `_thread` in Python 3. 28 import _thread as thread 29 30# Set this to True for additional output 31DEBUG_OUTPUT = True 32 33 34def print_debug(s): 35 """Print something if DEBUG_OUTPUT is True""" 36 global DEBUG_OUTPUT 37 if DEBUG_OUTPUT: 38 print("DEBUG: " + str(s)) 39 40 41def normalize_whitespace(s): 42 """Replace newlines, tabs, multiple spaces, etc with exactly one space""" 43 return re.sub("\s+", " ", s) 44 45 46def breakpoint_callback(frame, bp_loc, dict): 47 """This callback is registered with every breakpoint and makes sure that the 48 frame containing the breakpoint location is selected """ 49 50 # HACK(eddyb) print a newline to avoid continuing an unfinished line. 51 print("") 52 print("Hit breakpoint " + str(bp_loc)) 53 54 # Select the frame and the thread containing it 55 frame.thread.process.SetSelectedThread(frame.thread) 56 frame.thread.SetSelectedFrame(frame.idx) 57 58 # Returning True means that we actually want to stop at this breakpoint 59 return True 60 61 62# This is a list of breakpoints that are not registered with the breakpoint callback. The list is 63# populated by the breakpoint listener and checked/emptied whenever a command has been executed 64new_breakpoints = [] 65 66# This set contains all breakpoint ids that have already been registered with a callback, and is 67# used to avoid hooking callbacks into breakpoints more than once 68registered_breakpoints = set() 69 70 71def execute_command(command_interpreter, command): 72 """Executes a single CLI command""" 73 global new_breakpoints 74 global registered_breakpoints 75 76 res = lldb.SBCommandReturnObject() 77 print(command) 78 command_interpreter.HandleCommand(command, res) 79 80 if res.Succeeded(): 81 if res.HasResult(): 82 print(normalize_whitespace(res.GetOutput() or ''), end='\n') 83 84 # If the command introduced any breakpoints, make sure to register 85 # them with the breakpoint 86 # callback 87 while len(new_breakpoints) > 0: 88 res.Clear() 89 breakpoint_id = new_breakpoints.pop() 90 91 if breakpoint_id in registered_breakpoints: 92 print_debug("breakpoint with id %s is already registered. Ignoring." % 93 str(breakpoint_id)) 94 else: 95 print_debug("registering breakpoint callback, id = " + str(breakpoint_id)) 96 callback_command = ("breakpoint command add -F breakpoint_callback " + 97 str(breakpoint_id)) 98 command_interpreter.HandleCommand(callback_command, res) 99 if res.Succeeded(): 100 print_debug("successfully registered breakpoint callback, id = " + 101 str(breakpoint_id)) 102 registered_breakpoints.add(breakpoint_id) 103 else: 104 print("Error while trying to register breakpoint callback, id = " + 105 str(breakpoint_id) + ", message = " + str(res.GetError())) 106 else: 107 print(res.GetError()) 108 109 110def start_breakpoint_listener(target): 111 """Listens for breakpoints being added and adds new ones to the callback 112 registration list""" 113 listener = lldb.SBListener("breakpoint listener") 114 115 def listen(): 116 event = lldb.SBEvent() 117 try: 118 while True: 119 if listener.WaitForEvent(120, event): 120 if lldb.SBBreakpoint.EventIsBreakpointEvent(event) and \ 121 lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event) == \ 122 lldb.eBreakpointEventTypeAdded: 123 global new_breakpoints 124 breakpoint = lldb.SBBreakpoint.GetBreakpointFromEvent(event) 125 print_debug("breakpoint added, id = " + str(breakpoint.id)) 126 new_breakpoints.append(breakpoint.id) 127 except: 128 print_debug("breakpoint listener shutting down") 129 130 # Start the listener and let it run as a daemon 131 listener_thread = threading.Thread(target=listen) 132 listener_thread.daemon = True 133 listener_thread.start() 134 135 # Register the listener with the target 136 target.GetBroadcaster().AddListener(listener, lldb.SBTarget.eBroadcastBitBreakpointChanged) 137 138 139def start_watchdog(): 140 """Starts a watchdog thread that will terminate the process after a certain 141 period of time""" 142 143 try: 144 from time import clock 145 except ImportError: 146 from time import perf_counter as clock 147 148 watchdog_start_time = clock() 149 watchdog_max_time = watchdog_start_time + 30 150 151 def watchdog(): 152 while clock() < watchdog_max_time: 153 time.sleep(1) 154 print("TIMEOUT: lldb_batchmode.py has been running for too long. Aborting!") 155 thread.interrupt_main() 156 157 # Start the listener and let it run as a daemon 158 watchdog_thread = threading.Thread(target=watchdog) 159 watchdog_thread.daemon = True 160 watchdog_thread.start() 161 162#################################################################################################### 163# ~main 164#################################################################################################### 165 166 167if len(sys.argv) != 3: 168 print("usage: python lldb_batchmode.py target-path script-path") 169 sys.exit(1) 170 171target_path = sys.argv[1] 172script_path = sys.argv[2] 173 174print("LLDB batch-mode script") 175print("----------------------") 176print("Debugger commands script is '%s'." % script_path) 177print("Target executable is '%s'." % target_path) 178print("Current working directory is '%s'" % os.getcwd()) 179 180# Start the timeout watchdog 181start_watchdog() 182 183# Create a new debugger instance 184debugger = lldb.SBDebugger.Create() 185 186# When we step or continue, don't return from the function until the process 187# stops. We do this by setting the async mode to false. 188debugger.SetAsync(False) 189 190# Create a target from a file and arch 191print("Creating a target for '%s'" % target_path) 192target_error = lldb.SBError() 193target = debugger.CreateTarget(target_path, None, None, True, target_error) 194 195if not target: 196 print("Could not create debugging target '" + target_path + "': " + 197 str(target_error) + ". Aborting.", file=sys.stderr) 198 sys.exit(1) 199 200 201# Register the breakpoint callback for every breakpoint 202start_breakpoint_listener(target) 203 204command_interpreter = debugger.GetCommandInterpreter() 205 206try: 207 script_file = open(script_path, 'r') 208 209 for line in script_file: 210 command = line.strip() 211 if command == "run" or command == "r" or re.match("^process\s+launch.*", command): 212 # Before starting to run the program, let the thread sleep a bit, so all 213 # breakpoint added events can be processed 214 time.sleep(0.5) 215 if command != '': 216 execute_command(command_interpreter, command) 217 218except IOError as e: 219 print("Could not read debugging script '%s'." % script_path, file=sys.stderr) 220 print(e, file=sys.stderr) 221 print("Aborting.", file=sys.stderr) 222 sys.exit(1) 223finally: 224 debugger.Terminate() 225 script_file.close() 226