From e4bf38d3e5abd96fc811434929bff321a05ff458 Mon Sep 17 00:00:00 2001 From: Robert O'Callahan Date: Fri, 7 Jun 2024 19:13:27 +1200 Subject: [PATCH] Support RR extension commands in LLDB --- CMakeLists.txt | 1 + src/DebuggerExtensionCommand.cc | 7 +- src/DebuggerExtensionCommandHandler.cc | 120 ++++++++++++++++++++----- src/DebuggerExtensionCommandHandler.h | 10 ++- src/LldbInitCommand.cc | 35 ++++++++ src/launch_debugger.cc | 19 ++-- src/launch_debugger.h | 5 ++ src/test/util.py | 6 +- src/test/when.py | 48 +++++----- src/test/when.run | 2 +- 10 files changed, 193 insertions(+), 60 deletions(-) create mode 100644 src/LldbInitCommand.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ae012857a1..95ab4c2b5ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -592,6 +592,7 @@ set(RR_SOURCES src/kernel_metadata.cc src/launch_debugger.cc src/log.cc + src/LldbInitCommand.cc src/LsCommand.cc src/main.cc src/MagicSaveDataMonitor.cc diff --git a/src/DebuggerExtensionCommand.cc b/src/DebuggerExtensionCommand.cc index fb00de00981..fb2e216c52d 100644 --- a/src/DebuggerExtensionCommand.cc +++ b/src/DebuggerExtensionCommand.cc @@ -183,8 +183,11 @@ static SimpleDebuggerExtensionCommand info_checkpoints( "list all checkpoints created with the 'checkpoint' command", invoke_info_checkpoints); -/*static*/ void DebuggerExtensionCommand::init_auto_args() { - checkpoint.add_auto_arg("rr-where"); +void DebuggerExtensionCommand::init_auto_args() { + static __attribute__((unused)) int dummy = []() { + checkpoint.add_auto_arg("rr-where"); + return 0; + }(); } } // namespace rr diff --git a/src/DebuggerExtensionCommandHandler.cc b/src/DebuggerExtensionCommandHandler.cc index 7b2731a67e0..06f26a25ba3 100644 --- a/src/DebuggerExtensionCommandHandler.cc +++ b/src/DebuggerExtensionCommandHandler.cc @@ -15,32 +15,27 @@ namespace rr { // and linear search is fine. static vector* debugger_command_list; -static string gdb_macro_binding(const DebuggerExtensionCommand& cmd) { - string auto_args_str = "["; - for (size_t i = 0; i < cmd.auto_args().size(); i++) { +static void print_python_string_array(ostream& out, const std::vector& strs) { + out << "["; + for (size_t i = 0; i < strs.size(); i++) { if (i > 0) { - auto_args_str += ", "; + out << ", "; } - auto_args_str += "'" + cmd.auto_args()[i] + "'"; + out << "'" << strs[i] << "'"; } - auto_args_str += "]"; - string ret = "python RRCmd('" + cmd.name() + "', " + auto_args_str + ")\n"; + out << "]"; +} + +static void gdb_macro_binding(ostream& out, const DebuggerExtensionCommand& cmd) { + out << "python RRCmd('" << cmd.name() << "', "; + print_python_string_array(out, cmd.auto_args()); + out << ")\n"; if (!cmd.docs().empty()) { - ret += "document " + cmd.name() + "\n" + cmd.docs() + "\nend\n"; + out << "document " << cmd.name() << "\n" << cmd.docs() << "\nend\n"; } - return ret; } -/* static */ string DebuggerExtensionCommandHandler::gdb_macros() { - DebuggerExtensionCommand::init_auto_args(); - stringstream ss; - ss << string(R"Delimiter( - -set python print-stack full -python - -import re - +static const char shared_python[] = R"Delimiter( def hex_unescape(string): str_len = len(string) if str_len % 2: # check for unexpected string length @@ -64,6 +59,18 @@ def hex_escape(string): result += format(curr_char, '02x') return result +)Delimiter"; + +string DebuggerExtensionCommandHandler::gdb_macros() { + DebuggerExtensionCommand::init_auto_args(); + stringstream ss; + ss << R"Delimiter(set python print-stack full +python +)Delimiter" + << shared_python + << R"Delimiter( +import re + class RRWhere(gdb.Command): """Helper to get the location for checkpoints/history. Used by auto-args""" def __init__(self): @@ -163,11 +170,11 @@ RRSetSuppressRunHook() gdb.events.stop.connect(history_push) end -)Delimiter"); +)Delimiter"; if (debugger_command_list) { for (auto& it : *debugger_command_list) { - ss << gdb_macro_binding(*it); + gdb_macro_binding(ss, *it); } } @@ -186,6 +193,77 @@ end return ss.str(); } +static void lldb_macro_binding(ostream& ss, const DebuggerExtensionCommand& cmd) { + string func_name = "rr_command_"; + for (char ch : cmd.name()) { + if (ch == ' ') { + // We don't support commands with spaces in LLDB yet + return; + } + if (ch == '-') { + ch = '_'; + } + func_name.push_back(ch); + } + ss << "def " << func_name << "(debugger, command, exe_ctx, result, internal_dict):\n"; + if (!cmd.docs().empty()) { + ss << " \"\"\"" << cmd.docs() << "\"\"\"\n"; + } + ss << " cmd_name = '" << cmd.name() << "'\n" + << " auto_args = "; + print_python_string_array(ss, cmd.auto_args()); + ss << "\n" + << " command_impl(debugger, command, exe_ctx, result, cmd_name, auto_args)\n" + << "\n" + << "lldb.debugger.HandleCommand('command script add -f " << func_name + << " " << cmd.name() << "')\n\n"; +} + +string DebuggerExtensionCommandHandler::lldb_python_macros() { + DebuggerExtensionCommand::init_auto_args(); + stringstream ss; + ss << shared_python + << R"Delimiter( +import lldb +import re +import shlex + +def run_command_and_get_output(debugger, command): + result = lldb.SBCommandReturnObject() + debugger.GetCommandInterpreter().HandleCommand(command, result) + assert result.Succeeded() + return result.GetOutput() + +def command_impl(debugger, command, exe_ctx, result, cmd_name, auto_args): + interpreter = debugger.GetCommandInterpreter() + args = shlex.split(command) + # Ensure lldb tells rr its current thread + curr_thread = exe_ctx.thread + cmd_prefix = ("process plugin packet send qRRCmd:%s:%d"% + (cmd_name, -1 if curr_thread is None else curr_thread.GetThreadID())) + arg_strs = [] + for auto_arg in auto_args: + arg_strs.append(":" + hex_escape(run_command_and_get_output(debugger, auto_arg))) + for arg in args: + arg_strs.append(":" + hex_escape(arg)) + rv = run_command_and_get_output(debugger, cmd_prefix + ''.join(arg_strs)); + rv_match = re.search('response: (.*)$', rv, re.MULTILINE); + if not rv_match: + result.SetError(None, "Invalid response: %s" % rv) + return + response = hex_unescape(rv_match.group(1)) + result.Print(response.strip()) + +)Delimiter"; + + if (debugger_command_list) { + for (auto& it : *debugger_command_list) { + lldb_macro_binding(ss, *it); + } + } + return ss.str(); +} + /*static*/ DebuggerExtensionCommand* DebuggerExtensionCommandHandler::command_for_name(const string& name) { if (!debugger_command_list) { return nullptr; diff --git a/src/DebuggerExtensionCommandHandler.h b/src/DebuggerExtensionCommandHandler.h index d4a539fa0d1..2ac97f8a66c 100644 --- a/src/DebuggerExtensionCommandHandler.h +++ b/src/DebuggerExtensionCommandHandler.h @@ -14,19 +14,23 @@ class GdbServer; class Task; /** - * rr extends debuggers (GDB) with custom commands such as `when`. + * rr extends debuggers (GDB, LLDB) with custom commands such as `when`. * This class manages those commands. */ class DebuggerExtensionCommandHandler { public: // Declare any registered command with supporting - // wrapper code. + // wrapper code --- GDB script. static std::string gdb_macros(); + // Declare any registered command with supporting + // wrapper code --- LLDB Python script. + static std::string lldb_python_macros(); + static void register_command(DebuggerExtensionCommand& cmd); /** - * Process an incoming GDB payload of the following form: + * Process an incoming debugger payload of the following form: * :::... * * NOTE: RR Command are typically sent with the qRRCmd: prefix which diff --git a/src/LldbInitCommand.cc b/src/LldbInitCommand.cc new file mode 100644 index 00000000000..baf4a3f729c --- /dev/null +++ b/src/LldbInitCommand.cc @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; c-basic-offset: 2; indent-tabs-mode: nil; -*- */ + +#include "Command.h" +#include "GdbServer.h" +#include "launch_debugger.h" +#include "main.h" + +using namespace std; + +namespace rr { + +class LldbInitCommand : public Command { +public: + virtual int run(vector& args) override; + +protected: + LldbInitCommand(const char* name, const char* help) : Command(name, help) {} + + static LldbInitCommand singleton; +}; + +LldbInitCommand LldbInitCommand::singleton("lldbinit", " rr lldbinit\n"); + +int LldbInitCommand::run(vector& args) { + while (parse_global_option(args)) { + } + + puts("# This is a Python script. Save it to a file and run it from LLDB using"); + puts("# script exec(open('').read())"); + puts("# or similar."); + fputs(lldb_init_script().c_str(), stdout); + return 0; +} + +} // namespace rr diff --git a/src/launch_debugger.cc b/src/launch_debugger.cc index 9324e3bff0b..4717d5dc14a 100644 --- a/src/launch_debugger.cc +++ b/src/launch_debugger.cc @@ -138,15 +138,13 @@ static const string& gdb_rr_macros() { return s; } -// Special-sauce macros defined by rr when launching the gdb client, -// which implement functionality outside of the gdb remote protocol. -// (Don't stare at them too long or you'll go blind ;).) -static const string& lldb_rr_macros() { +static const string& lldb_python_rr_macros() { static string s; if (s.empty()) { stringstream ss; - ss << "set set prompt \"(rr) \"\n"; + ss << DebuggerExtensionCommandHandler::lldb_python_macros() + << "lldb.debugger.HandleCommand('set set prompt \"(rr) \"')\n"; s = ss.str(); } return s; @@ -332,9 +330,12 @@ void launch_debugger(ScopedFd& params_pipe_fd, } case DebuggerType::LLDB: { cmd.push_back("--source-quietly"); - string lldb_command_file = create_command_file(lldb_rr_macros()); - cmd.push_back("--source-before-file"); - cmd.push_back(lldb_command_file); + // We have to load the commands as a Python script. If we + // use the "script" command to launch a nested Python interpreter, + // Python emits some annoying text that we dont want to see. + string lldb_command_file = create_command_file(lldb_python_rr_macros()); + cmd.push_back("-o"); + cmd.push_back("script exec(open('" + lldb_command_file + "').read())"); cmd.insert(cmd.end(), options.begin(), options.end()); push_lldb_target_remote_cmd(cmd, socket_domain, host, port); env.push_back("LLDB_UNDER_RR=1"); @@ -403,4 +404,6 @@ void emergency_debug(Task* t) { string gdb_init_script() { return gdb_rr_macros(); } +string lldb_init_script() { return lldb_python_rr_macros(); } + } // namespace rr diff --git a/src/launch_debugger.h b/src/launch_debugger.h index fc8722cc563..b5e66b7cb83 100644 --- a/src/launch_debugger.h +++ b/src/launch_debugger.h @@ -55,6 +55,11 @@ void emergency_debug(Task* t); */ std::string gdb_init_script(); +/** + * A string containing the default lldbinit script that we load into lldb. + */ +std::string lldb_init_script(); + } // namespace rr #endif /* RR_LAUNCH_DEBUGGER_H_ */ diff --git a/src/test/util.py b/src/test/util.py index bf2490c1269..0a94331546a 100644 --- a/src/test/util.py +++ b/src/test/util.py @@ -9,7 +9,8 @@ 'delete_watchpoint', 'expect_signal_stop', 'set_breakpoint_commands', 'select_thread', 'scheduler_locking_on', 'scheduler_locking_off', - 'expect_expression', 'expect_threads', ] + 'expect_expression', 'expect_threads', + 'send_custom_command' ] # Don't use python timeout. Use test-monitor timeout instead. TIMEOUT_SEC = 10000 @@ -165,6 +166,9 @@ def scheduler_locking_off(): if debugger_type == 'GDB': send_gdb('set scheduler-locking off') +def send_custom_command(cmd): + send_debugger(cmd, cmd) + def send_debugger(gdb_cmd, lldb_cmd): if debugger_type == 'GDB': send_gdb(gdb_cmd) diff --git a/src/test/when.py b/src/test/when.py index 7d42931b975..e37b6fd71be 100644 --- a/src/test/when.py +++ b/src/test/when.py @@ -1,66 +1,66 @@ from util import * import re -send_gdb('when') -expect_gdb(re.compile(r'Completed event: (\d+)')) +send_custom_command('when') +expect_debugger(re.compile(r'Completed event: (\d+)')) t = int(last_match().group(1)) if t < 1 or t > 10000: failed('ERROR in first "when"') -send_gdb('when-ticks') -expect_gdb(re.compile(r'Current tick: (\d+)')) +send_custom_command('when-ticks') +expect_debugger(re.compile(r'Current tick: (\d+)')) ticks = int(last_match().group(1)) if ticks != 0: failed('ERROR in first "when-ticks"') -send_gdb('when-tid') -expect_gdb(re.compile(r'Current tid: (\d+)')) +send_custom_command('when-tid') +expect_debugger(re.compile(r'Current tid: (\d+)')) tid = int(last_match().group(1)) -send_gdb('b main') -expect_gdb('Breakpoint 1') -send_gdb('c') +bp = breakpoint_at_function('main') +cont() +expect_breakpoint_stop(bp) -send_gdb('when') -expect_gdb(re.compile(r'Completed event: (\d+)')) +send_custom_command('when') +expect_debugger(re.compile(r'Completed event: (\d+)')) t2 = int(last_match().group(1)) if t2 < 1 or t2 > 10000: failed('ERROR in second "when"') if t2 <= t: failed('ERROR ... "when" failed to advance') -send_gdb('when-ticks') -expect_gdb(re.compile(r'Current tick: (\d+)')) +send_custom_command('when-ticks') +expect_debugger(re.compile(r'Current tick: (\d+)')) ticks2 = int(last_match().group(1)) if ticks2 < 0 or ticks2 > 1000000: failed('ERROR in second "when-ticks"') if ticks2 <= ticks: failed('ERROR ... "when-ticks" failed to advance') -send_gdb('when-tid') -expect_gdb(re.compile(r'Current tid: (\d+)')) +send_custom_command('when-tid') +expect_debugger(re.compile(r'Current tid: (\d+)')) tid2 = int(last_match().group(1)) if tid2 != tid: failed('ERROR ... tid changed') # Ensure 'when' terminates a diversion -send_gdb('call strlen("abcd")') -send_gdb('when') -expect_gdb(re.compile(r'Completed event: (\d+)')) +expect_expression('(int)strlen("abcd")', 4) +send_custom_command('when') +expect_debugger(re.compile(r'Completed event: (\d+)')) t3 = int(last_match().group(1)) if t3 != t2: failed('ERROR ... diversion changed event') -send_gdb('call strlen("abcd")') -send_gdb('when-ticks') -expect_gdb(re.compile(r'Current tick: (\d+)')) +expect_expression('(int)strlen("abcd")', 4) +send_custom_command('when-ticks') +expect_debugger(re.compile(r'Current tick: (\d+)')) ticks3 = int(last_match().group(1)) if ticks3 != ticks2: failed('ERROR ... diversion changed ticks') -send_gdb('call strlen("abcd")') -send_gdb('when-tid') -expect_gdb(re.compile(r'Current tid: (\d+)')) +expect_expression('(int)strlen("abcd")', 4) +send_custom_command('when-tid') +expect_debugger(re.compile(r'Current tid: (\d+)')) tid3 = int(last_match().group(1)) if tid3 != tid2: failed('ERROR ... diversion changed tid') diff --git a/src/test/when.run b/src/test/when.run index df6344c91ac..6a3635d55d7 100644 --- a/src/test/when.run +++ b/src/test/when.run @@ -1,3 +1,3 @@ source `dirname $0`/util.sh record simple$bitness -debug_gdb_only when +debug when