https://github.com/felipepiovezan created https://github.com/llvm/llvm-project/pull/192914
This implements the packet as described in https://github.com/llvm/llvm-project/pull/192910 >From aca05b06ae90eb42a9c5b131d0a386f2037122e2 Mon Sep 17 00:00:00 2001 From: Felipe de Azevedo Piovezan <[email protected]> Date: Fri, 20 Mar 2026 14:06:16 +0000 Subject: [PATCH] [debugserver] Implement MultiBreakpoint This implements the packet as described in https://github.com/llvm/llvm-project/pull/192910 --- .../functionalities/multi-breakpoint/Makefile | 3 + .../multi-breakpoint/TestMultiBreakpoint.py | 173 ++++++++++++++++++ .../functionalities/multi-breakpoint/main.c | 7 + lldb/tools/debugserver/source/RNBRemote.cpp | 69 +++++++ lldb/tools/debugserver/source/RNBRemote.h | 2 + 5 files changed, 254 insertions(+) create mode 100644 lldb/test/API/functionalities/multi-breakpoint/Makefile create mode 100644 lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py create mode 100644 lldb/test/API/functionalities/multi-breakpoint/main.c diff --git a/lldb/test/API/functionalities/multi-breakpoint/Makefile b/lldb/test/API/functionalities/multi-breakpoint/Makefile new file mode 100644 index 0000000000000..10495940055b6 --- /dev/null +++ b/lldb/test/API/functionalities/multi-breakpoint/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py new file mode 100644 index 0000000000000..0978a499c7112 --- /dev/null +++ b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py @@ -0,0 +1,173 @@ +""" +Tests the MultiBreakpoint packet, this test runs against whichever debug server +the platform provides (debugserver on macOS, lldb-server elsewhere). +""" + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +@skipUnlessDarwin # Remove once lldbsever support is implemented. +@skipIfOutOfTreeDebugserver +class TestMultiBreakpoint(TestBase): + def send_packet(self, packet_str): + self.runCmd(f"process plugin packet send {packet_str}", check=False) + output = self.res.GetOutput() + reply = output.split("\n") + # The output is of the form: + # packet: <packet_str> + # response: <response> + packet_line = None + response_line = None + for line in reply: + line = line.strip() + if line.startswith("packet:"): + packet_line = line + elif line.startswith("response:"): + response_line = line + self.assertIsNotNone(packet_line, f"No 'packet:' line in output: {output}") + self.assertIsNotNone(response_line, f"No 'response:' line in output: {output}") + return response_line[len("response:") :].strip() + + def check_invalid_packet(self, packet_str): + """Assert that sending a malformed packet returns an error.""" + reply = self.send_packet(packet_str) + self.assertMultiResponse(reply, ["error"]) + + def assertMultiResponse(self, reply, expected): + """Assert a ';'-separated multi-response matches the expected pattern. + + Each element of `expected` is either "OK" for an exact match, or + "error" to accept any error response (starting with 'E').""" + parts = reply.split(";") + self.assertEqual(len(parts), len(expected), + f"Expected {len(expected)} responses, got {len(parts)}: {reply}") + for i, (actual, exp) in enumerate(zip(parts, expected)): + if exp == "OK": + self.assertEqual(actual, "OK", f"Response {i}: expected OK, got {actual}") + elif exp == "error": + self.assertTrue(actual.startswith("E"), + f"Response {i}: expected error, got {actual}") + else: + self.fail(f"Bad expected value '{exp}' at index {i}") + + def get_function_address(self, name): + """Return the hex address of a function as a string (no 0x prefix).""" + funcs = self.target.FindFunctions(name) + self.assertGreater(len(funcs), 0, f"Could not find function '{name}'") + addr = funcs[0].GetSymbol().GetStartAddress().GetLoadAddress(self.target) + self.assertNotEqual(addr, lldb.LLDB_INVALID_ADDRESS) + return f"{addr:x}" + + def test_multi_breakpoint(self): + self.build() + source_file = lldb.SBFileSpec("main.c") + self.target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, "break here", source_file + ) + + # Verify the server advertises MultiBreakpoint support. + reply = self.send_packet("qSupported") + self.assertIn("MultiBreakpoint+", reply) + + addr_a = self.get_function_address("func_a") + addr_b = self.get_function_address("func_b") + addr_c = self.get_function_address("func_c") + + # For breakpoint kind, use 4 on AArch64 (4-byte instruction), 1 elsewhere. + arch = self.getArchitecture() + if arch in ["arm64", "aarch64"]: + bp_kind = "4" + else: + bp_kind = "1" + + # --- Malformed packets --- + + # No colon. + self.check_invalid_packet("MultiBreakpoint") + # Empty body after colon. + self.check_invalid_packet("MultiBreakpoint:") + # Missing type digit. + self.check_invalid_packet(f"MultiBreakpoint:Z") + # Missing address. + self.check_invalid_packet(f"MultiBreakpoint:Z0,") + # Missing kind. + self.check_invalid_packet(f"MultiBreakpoint:Z0,{addr_a}") + # Missing kind (has trailing comma but no value). + self.check_invalid_packet(f"MultiBreakpoint:Z0,{addr_a},") + # Invalid stoppoint type. + self.check_invalid_packet(f"MultiBreakpoint:Z9,{addr_a},{bp_kind}") + # Completely garbled. + self.check_invalid_packet("MultiBreakpoint:hello") + # Just a semicolon. + self.check_invalid_packet("MultiBreakpoint:;") + + # --- Set a single breakpoint --- + reply = self.send_packet(f"MultiBreakpoint:Z0,{addr_a},{bp_kind}") + self.assertEqual(reply, "OK") + + # --- Remove the breakpoint we just set --- + reply = self.send_packet(f"MultiBreakpoint:z0,{addr_a},{bp_kind}") + self.assertEqual(reply, "OK") + + # --- Set multiple breakpoints at once --- + reply = self.send_packet( + f"MultiBreakpoint:Z0,{addr_a},{bp_kind};Z0,{addr_b},{bp_kind};Z0,{addr_c},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK;OK") + + # --- Remove multiple breakpoints at once --- + reply = self.send_packet( + f"MultiBreakpoint:z0,{addr_a},{bp_kind};z0,{addr_b},{bp_kind};z0,{addr_c},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK;OK") + + # --- Mixed set and remove in one packet --- + # Set two breakpoints first. + reply = self.send_packet( + f"MultiBreakpoint:Z0,{addr_a},{bp_kind};Z0,{addr_b},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK") + # Remove one, set another, remove the other. + reply = self.send_packet( + f"MultiBreakpoint:z0,{addr_a},{bp_kind};Z0,{addr_c},{bp_kind};z0,{addr_b},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK;OK") + # Clean up. + reply = self.send_packet(f"MultiBreakpoint:z0,{addr_c},{bp_kind}") + self.assertEqual(reply, "OK") + + # --- Set the same breakpoint twice + reply = self.send_packet( + f"MultiBreakpoint:Z0,{addr_a},{bp_kind};Z0,{addr_a},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK") + # Clean up both. + reply = self.send_packet( + f"MultiBreakpoint:z0,{addr_a},{bp_kind};z0,{addr_a},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK") + + # --- Set the same breakpoint twice, but remove it thrice. + reply = self.send_packet( + f"MultiBreakpoint:Z0,{addr_a},{bp_kind};Z0,{addr_a},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK") + reply = self.send_packet( + f"MultiBreakpoint:z0,{addr_a},{bp_kind};z0,{addr_a},{bp_kind};z0,{addr_a},{bp_kind}" + ) + self.assertMultiResponse(reply, ["OK", "OK", "error"]) + + # --- Set and remove the same address in a single packet --- + # The spec requires requests to be executed in order, so the set + # should succeed and the subsequent remove should find and clear it. + reply = self.send_packet( + f"MultiBreakpoint:Z0,{addr_a},{bp_kind};z0,{addr_a},{bp_kind}" + ) + self.assertEqual(reply, "OK;OK") + + # --- Remove a breakpoint that was never set --- + reply = self.send_packet(f"MultiBreakpoint:z0,{addr_b},{bp_kind}") + self.assertMultiResponse(reply, ["error"]) diff --git a/lldb/test/API/functionalities/multi-breakpoint/main.c b/lldb/test/API/functionalities/multi-breakpoint/main.c new file mode 100644 index 0000000000000..ffd59d00dc20b --- /dev/null +++ b/lldb/test/API/functionalities/multi-breakpoint/main.c @@ -0,0 +1,7 @@ +void func_a() {} +void func_b() {} +void func_c() {} + +int main() { + return 0; // break here +} diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp index 7f419ae53bfbe..7565ebba71dce 100644 --- a/lldb/tools/debugserver/source/RNBRemote.cpp +++ b/lldb/tools/debugserver/source/RNBRemote.cpp @@ -308,6 +308,10 @@ void RNBRemote::CreatePacketTable() { // `MultiMemRead` as an `M` packet. t.push_back(Packet(multi_mem_read, &RNBRemote::HandlePacket_MultiMemRead, NULL, "MultiMemRead", "Read multiple memory addresses")); + // Same ordering concern: `MultiBreakpoint` must come before the `M` packet. + t.push_back(Packet(multi_breakpoint, &RNBRemote::HandlePacket_MultiBreakpoint, + NULL, "MultiBreakpoint", + "Set/remove multiple breakpoints at once")); t.push_back(Packet(write_memory, &RNBRemote::HandlePacket_M, NULL, "M", "Write memory")); t.push_back(Packet(write_register, &RNBRemote::HandlePacket_P, NULL, "P", @@ -3275,6 +3279,70 @@ rnb_err_t RNBRemote::HandlePacket_MultiMemRead(const char *p) { return SendPacket(reply_stream.str()); } +/// Split a MultiBreakpoint packet body into individual breakpoint requests. A +/// ';' starts a new request only if it is followed by [Zz]. +static std::vector<std::string_view> +SplitBreakpointRequests(const std::string_view packet) { + std::vector<std::string_view> requests; + size_t packet_size = packet.size(); + size_t request_start = 0; + + // Look for `;[zZ]`. + for (size_t i = 0; i + 1 < packet_size; ++i) { + if (packet[i] != ';') + continue; + char next_char = packet[i + 1]; + if (next_char == 'Z' || next_char == 'z') { + requests.emplace_back(packet.substr(request_start, i - request_start)); + request_start = i + 1; + } + } + requests.emplace_back(packet.substr(request_start)); + return requests; +} + +rnb_err_t RNBRemote::HandlePacket_MultiBreakpoint(const char *p) { + const std::string_view packet_name("MultiBreakpoint:"); + std::string_view packet(p); + + if (!starts_with(packet, packet_name)) + return HandlePacket_ILLFORMED(__FILE__, __LINE__, p, + "Invalid MultiBreakpoint packet prefix"); + + packet.remove_prefix(packet_name.size()); + + if (packet.empty()) + return HandlePacket_ILLFORMED(__FILE__, __LINE__, p, + "MultiBreakpoint has no requests"); + + std::ostringstream reply_stream; + bool first = true; + for (std::string_view request : SplitBreakpointRequests(packet)) { + BreakpointResult result = + ExecuteBreakpointRequest(std::string(request).c_str()); + if (!first) + reply_stream << ";"; + switch (result.kind) { + case BreakpointResult::Kind::OK: + reply_stream << "OK"; + break; + case BreakpointResult::Kind::Error: { + char error_str[8]; + snprintf(error_str, sizeof(error_str), "E%02x", result.error_code); + reply_stream << error_str; + break; + } + case BreakpointResult::Kind::IllFormed: + case BreakpointResult::Kind::Unimplemented: + reply_stream << "E03"; + break; + } + first = false; + } + + return SendPacket(reply_stream.str()); +} + // Read memory, sent it up as binary data. // Usage: xADDR,LEN // ADDR and LEN are both base 16. @@ -3629,6 +3697,7 @@ rnb_err_t RNBRemote::HandlePacket_qSupported(const char *p) { reply << "memory-tagging+;"; reply << "MultiMemRead+;"; + reply << "MultiBreakpoint+;"; return SendPacket(reply.str().c_str()); } diff --git a/lldb/tools/debugserver/source/RNBRemote.h b/lldb/tools/debugserver/source/RNBRemote.h index 5e306d889ba1c..088514b20fe16 100644 --- a/lldb/tools/debugserver/source/RNBRemote.h +++ b/lldb/tools/debugserver/source/RNBRemote.h @@ -137,6 +137,7 @@ class RNBRemote { json_query_dyld_process_state, // 'jGetDyldProcessState' enable_error_strings, // 'QEnableErrorStrings' multi_mem_read, // 'MultiMemRead' + multi_breakpoint, // 'MultiBreakpoint' unknown_type }; // clang-format on @@ -218,6 +219,7 @@ class RNBRemote { rnb_err_t HandlePacket_m(const char *p); rnb_err_t HandlePacket_M(const char *p); rnb_err_t HandlePacket_MultiMemRead(const char *p); + rnb_err_t HandlePacket_MultiBreakpoint(const char *p); rnb_err_t HandlePacket_x(const char *p); rnb_err_t HandlePacket_X(const char *p); rnb_err_t HandlePacket_z(const char *p); _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
