From 88292430efd1d01c1e31d43fe5114cae83611f41 Mon Sep 17 00:00:00 2001 From: Daniel Yang Date: Tue, 25 Feb 2025 01:23:55 -0800 Subject: [PATCH] Added LLDB python interface examples into debugger.py. I copied from LLVM source repo process_events.py. The API is the same as C++ but we should consider using python because testing in C++ is so annoying. Interface-wise the APIs are basically 1:1 mappings. --- CMakeLists.txt | 1 + debugger/CMakeLists.txt | 18 ++ debugger/debugger.cpp | 40 ++++ debugger/debugger.py | 427 +++++++++++++++++++++++++++++++++++++ debugger/testprog/main.cpp | 10 + 5 files changed, 496 insertions(+) create mode 100644 debugger/CMakeLists.txt create mode 100644 debugger/debugger.cpp create mode 100755 debugger/debugger.py create mode 100644 debugger/testprog/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a08593..859177a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,3 +18,4 @@ add_library(${LIB_NAME} STATIC ${LIB_SOURCES}) target_include_directories(${LIB_NAME} PUBLIC include) add_subdirectory(src) +add_subdirectory(debugger) diff --git a/debugger/CMakeLists.txt b/debugger/CMakeLists.txt new file mode 100644 index 0000000..8ce0038 --- /dev/null +++ b/debugger/CMakeLists.txt @@ -0,0 +1,18 @@ +# STEPS TO SET UP ENVIRONMENT: +# 1. Install lldb and liblldb-dev +# 2. Check if you get "error: unable to locate lldb-server-14.0.0" when running +# 3. If you get the above error, create a symlink from lldb-server-14.0.0 to lldb-server + + +cmake_minimum_required(VERSION 3.15) +project(debugger) + +# Set LLDB include and library paths manually +include_directories(/usr/include/lldb) +link_directories(/usr/lib/liblldb.so) + +# Add executable +add_executable(my_lldb_app debugger.cpp) + +# Link LLDB dynamically +target_link_libraries(my_lldb_app PRIVATE lldb) diff --git a/debugger/debugger.cpp b/debugger/debugger.cpp new file mode 100644 index 0000000..1b7e0b4 --- /dev/null +++ b/debugger/debugger.cpp @@ -0,0 +1,40 @@ +#include +#include + +using namespace lldb; + +int main() { + SBDebugger::Initialize(); + auto debugger = SBDebugger::Create(true); + debugger.SetAsync(false); + + auto target = debugger.CreateTarget("a.out"); + if (!target.IsValid()) { + std::cerr << "Failed to create target.\n"; + return 1; + } + + auto breakpoint = target.BreakpointCreateByLocation("main.cpp", 5); // Breakpoint location in the program being debugged + if (!breakpoint.IsValid()) { + std::cerr << "Failed to create breakpoint.\n"; + return 1; + } + + std::cout << "Printing works" << std::endl; + + auto process = target.LaunchSimple(nullptr, nullptr, nullptr); + if (!process.IsValid()) { + std::cerr << "Failed to launch process.\n"; + return 1; + } + + std::cout << "Printing works" << std::endl; + + auto info = process.GetProcessInfo(); + std::cout << "ProcessID: " << info.GetProcessID() << std::endl; + std::cout << "Name: " << info.GetName() << std::endl; + + SBDebugger::Terminate(); + return 0; +} + diff --git a/debugger/debugger.py b/debugger/debugger.py new file mode 100755 index 0000000..46fdd6c --- /dev/null +++ b/debugger/debugger.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python + +# ---------------------------------------------------------------------- +# Be sure to add the python path that points to the LLDB shared library. +# On MacOSX csh, tcsh: +# setenv PYTHONPATH /Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python +# On MacOSX sh, bash: +# export PYTHONPATH=/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python +# ---------------------------------------------------------------------- + +import optparse +import os +import platform +import sys +import subprocess +import lldb + +def print_threads(process, options): + if options.show_threads: + for thread in process: + print("%s %s" % (thread, thread.GetFrameAtIndex(0))) + + +def run_commands(command_interpreter, commands): + return_obj = lldb.SBCommandReturnObject() + for command in commands: + command_interpreter.HandleCommand(command, return_obj) + if return_obj.Succeeded(): + print(return_obj.GetOutput()) + else: + print(return_obj) + if options.stop_on_error: + break + + +def main(argv): + description = """Debugs a program using the LLDB python API and uses asynchronous broadcast events to watch for process state changes.""" + epilog = """Examples: + +#---------------------------------------------------------------------- +# Run "/bin/ls" with the arguments "-lAF /tmp/", and set a breakpoint +# at "malloc" and backtrace and read all registers each time we stop +#---------------------------------------------------------------------- +% ./process_events.py --breakpoint malloc --stop-command bt --stop-command 'register read' -- /bin/ls -lAF /tmp/ + +""" + optparse.OptionParser.format_epilog = lambda self, formatter: self.epilog + parser = optparse.OptionParser( + description=description, + prog="process_events", + usage="usage: process_events [options] program [arg1 arg2]", + epilog=epilog, + ) + parser.add_option( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Enable verbose logging.", + default=False, + ) + parser.add_option( + "-b", + "--breakpoint", + action="append", + type="string", + metavar="BPEXPR", + dest="breakpoints", + help='Breakpoint commands to create after the target has been created, the values will be sent to the "_regexp-break" command which supports breakpoints by name, file:line, and address.', + ) + parser.add_option( + "-a", + "--arch", + type="string", + dest="arch", + help="The architecture to use when creating the debug target.", + default=None, + ) + parser.add_option( + "--platform", + type="string", + metavar="platform", + dest="platform", + help='Specify the platform to use when creating the debug target. Valid values include "localhost", "darwin-kernel", "ios-simulator", "remote-freebsd", "remote-macosx", "remote-ios", "remote-linux".', + default=None, + ) + parser.add_option( + "-l", + "--launch-command", + action="append", + type="string", + metavar="CMD", + dest="launch_commands", + help="LLDB command interpreter commands to run once after the process has launched. This option can be specified more than once.", + default=[], + ) + parser.add_option( + "-s", + "--stop-command", + action="append", + type="string", + metavar="CMD", + dest="stop_commands", + help="LLDB command interpreter commands to run each time the process stops. This option can be specified more than once.", + default=[], + ) + parser.add_option( + "-c", + "--crash-command", + action="append", + type="string", + metavar="CMD", + dest="crash_commands", + help="LLDB command interpreter commands to run in case the process crashes. This option can be specified more than once.", + default=[], + ) + parser.add_option( + "-x", + "--exit-command", + action="append", + type="string", + metavar="CMD", + dest="exit_commands", + help="LLDB command interpreter commands to run once after the process has exited. This option can be specified more than once.", + default=[], + ) + parser.add_option( + "-T", + "--no-threads", + action="store_false", + dest="show_threads", + help="Don't show threads when process stops.", + default=True, + ) + parser.add_option( + "--ignore-errors", + action="store_false", + dest="stop_on_error", + help="Don't stop executing LLDB commands if the command returns an error. This applies to all of the LLDB command interpreter commands that get run for launch, stop, crash and exit.", + default=True, + ) + parser.add_option( + "-n", + "--run-count", + type="int", + dest="run_count", + metavar="N", + help="How many times to run the process in case the process exits.", + default=1, + ) + parser.add_option( + "-t", + "--event-timeout", + type="int", + dest="event_timeout", + metavar="SEC", + help="Specify the timeout in seconds to wait for process state change events.", + default=lldb.UINT32_MAX, + ) + parser.add_option( + "-e", + "--environment", + action="append", + type="string", + metavar="ENV", + dest="env_vars", + help="Environment variables to set in the inferior process when launching a process.", + ) + parser.add_option( + "-d", + "--working-dir", + type="string", + metavar="DIR", + dest="working_dir", + help="The current working directory when launching a process.", + default=None, + ) + parser.add_option( + "-p", + "--attach-pid", + type="int", + dest="attach_pid", + metavar="PID", + help="Specify a process to attach to by process ID.", + default=-1, + ) + parser.add_option( + "-P", + "--attach-name", + type="string", + dest="attach_name", + metavar="PROCESSNAME", + help="Specify a process to attach to by name.", + default=None, + ) + parser.add_option( + "-w", + "--attach-wait", + action="store_true", + dest="attach_wait", + help="Wait for the next process to launch when attaching to a process by name.", + default=False, + ) + try: + (options, args) = parser.parse_args(argv) + except: + return + + attach_info = None + launch_info = None + exe = None + if args: + exe = args.pop(0) + launch_info = lldb.SBLaunchInfo(args) + if options.env_vars: + launch_info.SetEnvironmentEntries(options.env_vars, True) + if options.working_dir: + launch_info.SetWorkingDirectory(options.working_dir) + elif options.attach_pid != -1: + if options.run_count == 1: + attach_info = lldb.SBAttachInfo(options.attach_pid) + else: + print("error: --run-count can't be used with the --attach-pid option") + sys.exit(1) + elif not options.attach_name is None: + if options.run_count == 1: + attach_info = lldb.SBAttachInfo(options.attach_name, options.attach_wait) + else: + print("error: --run-count can't be used with the --attach-name option") + sys.exit(1) + else: + print( + "error: a program path for a program to debug and its arguments are required" + ) + sys.exit(1) + + # Create a new debugger instance + debugger = lldb.SBDebugger.Create() + debugger.SetAsync(True) + command_interpreter = debugger.GetCommandInterpreter() + # Create a target from a file and arch + + if exe: + print("Creating a target for '%s'" % exe) + error = lldb.SBError() + target = debugger.CreateTarget(exe, options.arch, options.platform, True, error) + + if target: + # Set any breakpoints that were specified in the args if we are launching. We use the + # command line command to take advantage of the shorthand breakpoint + # creation + if launch_info and options.breakpoints: + for bp in options.breakpoints: + debugger.HandleCommand("_regexp-break %s" % (bp)) + run_commands(command_interpreter, ["breakpoint list"]) + + for run_idx in range(options.run_count): + # Launch the process. Since we specified synchronous mode, we won't return + # from this function until we hit the breakpoint at main + error = lldb.SBError() + + if launch_info: + if options.run_count == 1: + print('Launching "%s"...' % (exe)) + else: + print( + 'Launching "%s"... (launch %u of %u)' + % (exe, run_idx + 1, options.run_count) + ) + + process = target.Launch(launch_info, error) + else: + if options.attach_pid != -1: + print("Attaching to process %i..." % (options.attach_pid)) + else: + if options.attach_wait: + print( + 'Waiting for next to process named "%s" to launch...' + % (options.attach_name) + ) + else: + print( + 'Attaching to existing process named "%s"...' + % (options.attach_name) + ) + process = target.Attach(attach_info, error) + + # Make sure the launch went ok + if process and process.GetProcessID() != lldb.LLDB_INVALID_PROCESS_ID: + pid = process.GetProcessID() + print("Process is %i" % (pid)) + if attach_info: + # continue process if we attached as we won't get an + # initial event + process.Continue() + + listener = debugger.GetListener() + # sign up for process state change events + stop_idx = 0 + done = False + while not done: + event = lldb.SBEvent() + if listener.WaitForEvent(options.event_timeout, event): + if lldb.SBProcess.EventIsProcessEvent(event): + state = lldb.SBProcess.GetStateFromEvent(event) + if state == lldb.eStateInvalid: + # Not a state event + print("process event = %s" % (event)) + else: + print( + "process state changed event: %s" + % (lldb.SBDebugger.StateAsCString(state)) + ) + if state == lldb.eStateStopped: + if stop_idx == 0: + if launch_info: + print("process %u launched" % (pid)) + run_commands( + command_interpreter, ["breakpoint list"] + ) + else: + print("attached to process %u" % (pid)) + for m in target.modules: + print(m) + if options.breakpoints: + for bp in options.breakpoints: + debugger.HandleCommand( + "_regexp-break %s" % (bp) + ) + run_commands( + command_interpreter, + ["breakpoint list"], + ) + run_commands( + command_interpreter, options.launch_commands + ) + else: + if options.verbose: + print("process %u stopped" % (pid)) + run_commands( + command_interpreter, options.stop_commands + ) + stop_idx += 1 + print_threads(process, options) + print("continuing process %u" % (pid)) + process.Continue() + elif state == lldb.eStateExited: + exit_desc = process.GetExitDescription() + if exit_desc: + print( + "process %u exited with status %u: %s" + % (pid, process.GetExitStatus(), exit_desc) + ) + else: + print( + "process %u exited with status %u" + % (pid, process.GetExitStatus()) + ) + run_commands( + command_interpreter, options.exit_commands + ) + done = True + elif state == lldb.eStateCrashed: + print("process %u crashed" % (pid)) + print_threads(process, options) + run_commands( + command_interpreter, options.crash_commands + ) + done = True + elif state == lldb.eStateDetached: + print("process %u detached" % (pid)) + done = True + elif state == lldb.eStateRunning: + # process is running, don't say anything, + # we will always get one of these after + # resuming + if options.verbose: + print("process %u resumed" % (pid)) + elif state == lldb.eStateUnloaded: + print( + "process %u unloaded, this shouldn't happen" + % (pid) + ) + done = True + elif state == lldb.eStateConnected: + print("process connected") + elif state == lldb.eStateAttaching: + print("process attaching") + elif state == lldb.eStateLaunching: + print("process launching") + else: + print("event = %s" % (event)) + else: + # timeout waiting for an event + print( + "no process event for %u seconds, killing the process..." + % (options.event_timeout) + ) + done = True + # Now that we are done dump the stdout and stderr + process_stdout = process.GetSTDOUT(1024) + if process_stdout: + print("Process STDOUT:\n%s" % (process_stdout)) + while process_stdout: + process_stdout = process.GetSTDOUT(1024) + print(process_stdout) + process_stderr = process.GetSTDERR(1024) + if process_stderr: + print("Process STDERR:\n%s" % (process_stderr)) + while process_stderr: + process_stderr = process.GetSTDERR(1024) + print(process_stderr) + process.Kill() # kill the process + else: + if error: + print(error) + else: + if launch_info: + print("error: launch failed") + else: + print("error: attach failed") + + lldb.SBDebugger.Terminate() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/debugger/testprog/main.cpp b/debugger/testprog/main.cpp new file mode 100644 index 0000000..233c145 --- /dev/null +++ b/debugger/testprog/main.cpp @@ -0,0 +1,10 @@ +#include +#include + +int main() { + while (true) { + std::cout << "Test program" << std::endl; // Here you need to set a breakpoint + sleep(5); + } + return 0; +}