[llvm-branch-commits] [lldb] [debugserver] Implement MultiBreakpoint (PR #192914)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Mon Apr 20 01:27:43 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lldb
Author: Felipe de Azevedo Piovezan (felipepiovezan)
<details>
<summary>Changes</summary>
This implements the packet as described in https://github.com/llvm/llvm-project/pull/192910
---
Full diff: https://github.com/llvm/llvm-project/pull/192914.diff
5 Files Affected:
- (added) lldb/test/API/functionalities/multi-breakpoint/Makefile (+3)
- (added) lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py (+173)
- (added) lldb/test/API/functionalities/multi-breakpoint/main.c (+7)
- (modified) lldb/tools/debugserver/source/RNBRemote.cpp (+69)
- (modified) lldb/tools/debugserver/source/RNBRemote.h (+2)
``````````diff
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
+
+
+ at skipUnlessDarwin # Remove once lldbsever support is implemented.
+ at 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);
``````````
</details>
https://github.com/llvm/llvm-project/pull/192914
More information about the llvm-branch-commits
mailing list