# This script allows to use LLDB in a way similar to GDB's batch mode. That is, given a text file # containing LLDB commands (one command per line), this script will execute the commands one after # the other. # LLDB also has the -s and -S commandline options which also execute a list of commands from a text # file. However, this command are execute `immediately`: the command of a `run` or `continue` # command will be executed immediately after the `run` or `continue`, without waiting for the next # breakpoint to be hit. This a command sequence like the following will not yield reliable results: # # break 11 # run # print x # # Most of the time the `print` command will be executed while the program is still running will thus # fail. Using this Python script, the above will work as expected. from __future__ import print_function import lldb import os import sys import threading import re import time try: import thread except ModuleNotFoundError: # The `thread` module was renamed to `_thread` in Python 3. import _thread as thread # Set this to True for additional output DEBUG_OUTPUT = True def print_debug(s): """Print something if DEBUG_OUTPUT is True""" global DEBUG_OUTPUT if DEBUG_OUTPUT: print("DEBUG: " + str(s)) def normalize_whitespace(s): """Replace newlines, tabs, multiple spaces, etc with exactly one space""" return re.sub("\s+", " ", s) def breakpoint_callback(frame, bp_loc, dict): """This callback is registered with every breakpoint and makes sure that the frame containing the breakpoint location is selected """ # HACK(eddyb) print a newline to avoid continuing an unfinished line. print("") print("Hit breakpoint " + str(bp_loc)) # Select the frame and the thread containing it frame.thread.process.SetSelectedThread(frame.thread) frame.thread.SetSelectedFrame(frame.idx) # Returning True means that we actually want to stop at this breakpoint return True # This is a list of breakpoints that are not registered with the breakpoint callback. The list is # populated by the breakpoint listener and checked/emptied whenever a command has been executed new_breakpoints = [] # This set contains all breakpoint ids that have already been registered with a callback, and is # used to avoid hooking callbacks into breakpoints more than once registered_breakpoints = set() def execute_command(command_interpreter, command): """Executes a single CLI command""" global new_breakpoints global registered_breakpoints res = lldb.SBCommandReturnObject() print(command) command_interpreter.HandleCommand(command, res) if res.Succeeded(): if res.HasResult(): print(normalize_whitespace(res.GetOutput() or ''), end='\n') # If the command introduced any breakpoints, make sure to register # them with the breakpoint # callback while len(new_breakpoints) > 0: res.Clear() breakpoint_id = new_breakpoints.pop() if breakpoint_id in registered_breakpoints: print_debug("breakpoint with id %s is already registered. Ignoring." % str(breakpoint_id)) else: print_debug("registering breakpoint callback, id = " + str(breakpoint_id)) callback_command = ("breakpoint command add -F breakpoint_callback " + str(breakpoint_id)) command_interpreter.HandleCommand(callback_command, res) if res.Succeeded(): print_debug("successfully registered breakpoint callback, id = " + str(breakpoint_id)) registered_breakpoints.add(breakpoint_id) else: print("Error while trying to register breakpoint callback, id = " + str(breakpoint_id) + ", message = " + str(res.GetError())) else: print(res.GetError()) def start_breakpoint_listener(target): """Listens for breakpoints being added and adds new ones to the callback registration list""" listener = lldb.SBListener("breakpoint listener") def listen(): event = lldb.SBEvent() try: while True: if listener.WaitForEvent(120, event): if lldb.SBBreakpoint.EventIsBreakpointEvent(event) and \ lldb.SBBreakpoint.GetBreakpointEventTypeFromEvent(event) == \ lldb.eBreakpointEventTypeAdded: global new_breakpoints breakpoint = lldb.SBBreakpoint.GetBreakpointFromEvent(event) print_debug("breakpoint added, id = " + str(breakpoint.id)) new_breakpoints.append(breakpoint.id) except BaseException: # explicitly catch ctrl+c/sysexit print_debug("breakpoint listener shutting down") # Start the listener and let it run as a daemon listener_thread = threading.Thread(target=listen) listener_thread.daemon = True listener_thread.start() # Register the listener with the target target.GetBroadcaster().AddListener(listener, lldb.SBTarget.eBroadcastBitBreakpointChanged) def start_watchdog(): """Starts a watchdog thread that will terminate the process after a certain period of time""" try: from time import clock except ImportError: from time import perf_counter as clock watchdog_start_time = clock() watchdog_max_time = watchdog_start_time + 30 def watchdog(): while clock() < watchdog_max_time: time.sleep(1) print("TIMEOUT: lldb_batchmode.py has been running for too long. Aborting!") thread.interrupt_main() # Start the listener and let it run as a daemon watchdog_thread = threading.Thread(target=watchdog) watchdog_thread.daemon = True watchdog_thread.start() #################################################################################################### # ~main #################################################################################################### if len(sys.argv) != 3: print("usage: python lldb_batchmode.py target-path script-path") sys.exit(1) target_path = sys.argv[1] script_path = sys.argv[2] print("LLDB batch-mode script") print("----------------------") print("Debugger commands script is '%s'." % script_path) print("Target executable is '%s'." % target_path) print("Current working directory is '%s'" % os.getcwd()) # Start the timeout watchdog start_watchdog() # Create a new debugger instance debugger = lldb.SBDebugger.Create() # When we step or continue, don't return from the function until the process # stops. We do this by setting the async mode to false. debugger.SetAsync(False) # Create a target from a file and arch print("Creating a target for '%s'" % target_path) target_error = lldb.SBError() target = debugger.CreateTarget(target_path, None, None, True, target_error) if not target: print("Could not create debugging target '" + target_path + "': " + str(target_error) + ". Aborting.", file=sys.stderr) sys.exit(1) # Register the breakpoint callback for every breakpoint start_breakpoint_listener(target) command_interpreter = debugger.GetCommandInterpreter() try: script_file = open(script_path, 'r') for line in script_file: command = line.strip() if command == "run" or command == "r" or re.match("^process\s+launch.*", command): # Before starting to run the program, let the thread sleep a bit, so all # breakpoint added events can be processed time.sleep(0.5) if command != '': execute_command(command_interpreter, command) except IOError as e: print("Could not read debugging script '%s'." % script_path, file=sys.stderr) print(e, file=sys.stderr) print("Aborting.", file=sys.stderr) sys.exit(1) finally: debugger.Terminate() script_file.close()