[Lldb-commits] [lldb] [debugserver] Implement MultiBreakpoint (PR #192914)
Felipe de Azevedo Piovezan via lldb-commits
lldb-commits at lists.llvm.org
Mon Apr 27 07:07:33 PDT 2026
https://github.com/felipepiovezan updated https://github.com/llvm/llvm-project/pull/192914
>From 012f32be886b27a392fcaf22458c379aa642e8cb Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Fri, 20 Mar 2026 14:06:16 +0000
Subject: [PATCH 1/3] [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 | 191 ++++++++++++++++++
.../functionalities/multi-breakpoint/main.c | 7 +
lldb/tools/debugserver/source/JSON.h | 2 +
lldb/tools/debugserver/source/RNBRemote.cpp | 71 +++++++
lldb/tools/debugserver/source/RNBRemote.h | 2 +
6 files changed, 276 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..92bdd0254ec42
--- /dev/null
+++ b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
@@ -0,0 +1,191 @@
+"""
+Tests the jMultiBreakpoint packet, this test runs against whichever debug server
+the platform provides (debugserver on macOS, lldb-server elsewhere).
+"""
+
+import json
+
+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):
+ reply = self.send_packet(packet_str)
+ if reply.startswith("E"):
+ return
+ else:
+ self.assertMultiResponse(reply, ["error"])
+
+ def assertMultiResponse(self, reply, expected):
+ """Assert a JSON-array 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 = json.loads(reply)["results"]
+ 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 jMultiBreakpoint support.
+ reply = self.send_packet("qSupported")
+ self.assertIn("jMultiBreakpoint+", 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 ---
+ # Very light error testing, as debugserver and lldb-server behave
+ # somewhat differently under malformed input.
+
+ # Empty body after colon.
+ self.check_invalid_packet("jMultiBreakpoint:")
+ # Not a dictionary
+ self.check_invalid_packet((f'jMultiBreakpoint:["Z0,{addr_a},{bp_kind}"]'))
+
+ def make_packet(array):
+ key = "breakpoint_requests"
+ json_str = json.dumps({key: array})
+ return f"jMultiBreakpoint: {json_str}"
+
+ # --- Set a single breakpoint ---
+ array = [f"Z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK"])
+
+ # --- Remove the breakpoint we just set ---
+ array = [f"z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK"])
+
+ # --- Set multiple breakpoints at once ---
+ array = [
+ f"Z0,{addr_a},{bp_kind}",
+ f"Z0,{addr_b},{bp_kind}",
+ f"Z0,{addr_c},{bp_kind}",
+ ]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK", "OK"])
+
+ # --- Remove multiple breakpoints at once ---
+ array = [
+ f"z0,{addr_a},{bp_kind}",
+ f"z0,{addr_b},{bp_kind}",
+ f"z0,{addr_c},{bp_kind}",
+ ]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK", "OK"])
+
+ # --- Mixed set and remove in one packet ---
+ # Set two breakpoints first.
+ array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_b},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK"])
+
+ # Remove one, set another, remove the other.
+ array = [
+ f"z0,{addr_a},{bp_kind}",
+ f"Z0,{addr_c},{bp_kind}",
+ f"z0,{addr_b},{bp_kind}",
+ ]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK", "OK"])
+
+ # Clean up.
+ array = [f"z0,{addr_c},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK"])
+
+ # --- Set the same breakpoint twice
+ array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK"])
+
+ # Clean up both.
+ array = [f"z0,{addr_a},{bp_kind}", f"z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK"])
+
+ # --- Set the same breakpoint twice, but remove it thrice.
+ array = [f"Z0,{addr_a},{bp_kind}", f"Z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK"])
+ array = [
+ f"z0,{addr_a},{bp_kind}",
+ f"z0,{addr_a},{bp_kind}",
+ f"z0,{addr_a},{bp_kind}",
+ ]
+ reply = self.send_packet(make_packet(array))
+ 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.
+ array = [f"Z0,{addr_a},{bp_kind}", f"z0,{addr_a},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "OK"])
+
+ # --- Remove a breakpoint that was never set ---
+ array = [f"z0,{addr_b},{bp_kind}"]
+ reply = self.send_packet(make_packet(array))
+ 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/JSON.h b/lldb/tools/debugserver/source/JSON.h
index f118dcdb7311b..aaef3ed5c0951 100644
--- a/lldb/tools/debugserver/source/JSON.h
+++ b/lldb/tools/debugserver/source/JSON.h
@@ -258,6 +258,8 @@ class JSONArray : public JSONValue {
Size GetNumElements();
+ const Vector &Elements() const { return m_elements; }
+
~JSONArray() override = default;
Vector m_elements;
diff --git a/lldb/tools/debugserver/source/RNBRemote.cpp b/lldb/tools/debugserver/source/RNBRemote.cpp
index c740dcd3f9b1e..111f037ae9e79 100644
--- a/lldb/tools/debugserver/source/RNBRemote.cpp
+++ b/lldb/tools/debugserver/source/RNBRemote.cpp
@@ -481,6 +481,9 @@ void RNBRemote::CreatePacketTable() {
"jGetSharedCacheInfo", "Replies with JSON data about the "
"location and uuid of the shared "
"cache in the inferior process."));
+ t.push_back(Packet(
+ json_multi_breakpoint, &RNBRemote::HandlePacket_jMultiBreakpoint, NULL,
+ "jMultiBreakpoint", "Set/remove multiple breakpoints at once"));
t.push_back(Packet(start_noack_mode, &RNBRemote::HandlePacket_QStartNoAckMode,
NULL, "QStartNoAckMode",
"Request that " DEBUGSERVER_PROGRAM_NAME
@@ -3306,6 +3309,73 @@ rnb_err_t RNBRemote::HandlePacket_MultiMemRead(const char *p) {
return SendPacket(reply_stream.str());
}
+rnb_err_t RNBRemote::HandlePacket_jMultiBreakpoint(const char *p) {
+ const std::string_view packet_name("jMultiBreakpoint:");
+ 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());
+
+ JSONParser parser(packet.cbegin());
+ JSONValue::SP parsed = parser.ParseJSONValue();
+ if (!parsed || parsed->GetKind() != JSONValue::Kind::Object)
+ return HandlePacket_ILLFORMED(
+ __FILE__, __LINE__, p,
+ "MultiBreakpoint did not contain a JSON dictionary");
+
+ auto *request_dict = static_cast<JSONObject *>(parsed.get());
+ JSONValue::SP request_array_sp =
+ request_dict->GetObject("breakpoint_requests");
+
+ if (!request_array_sp ||
+ request_array_sp->GetKind() != JSONValue::Kind::Array)
+ return HandlePacket_ILLFORMED(
+ __FILE__, __LINE__, p,
+ "MultiBreakpoint did not contain a valid 'breakpoint_requests' field");
+
+ auto *request_array = static_cast<JSONArray *>(request_array_sp.get());
+ std::vector<std::string> requests;
+ requests.reserve(request_array->GetNumElements());
+ for (JSONValue::SP value : request_array->Elements()) {
+ if (!value || value->GetKind() != JSONValue::Kind::String)
+ return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
+ "MultiBreakpoint had a non-string entry");
+ auto *request_str = static_cast<JSONString *>(value.get());
+ requests.push_back(request_str->GetData());
+ }
+
+ auto reply_array = std::make_shared<JSONGenerator::Array>();
+ for (const std::string &request : requests) {
+ BreakpointResult result = ExecuteBreakpointRequest(request.c_str());
+ std::string reply_str;
+ switch (result.kind) {
+ case BreakpointResult::Kind::OK:
+ reply_str = "OK";
+ break;
+ case BreakpointResult::Kind::Error: {
+ char error_str[8];
+ snprintf(error_str, sizeof(error_str), "E%02x", result.error_code);
+ reply_str = error_str;
+ break;
+ }
+ case BreakpointResult::Kind::IllFormed:
+ case BreakpointResult::Kind::Unimplemented:
+ reply_str = "E03";
+ break;
+ }
+ reply_array->AddItem(std::make_shared<JSONGenerator::String>(reply_str));
+ }
+
+ JSONGenerator::Dictionary reply_dict;
+ reply_dict.AddItem("results", reply_array);
+ std::ostringstream reply_stream;
+ reply_dict.DumpBinaryEscaped(reply_stream);
+ return SendPacket(reply_stream.str());
+}
+
// Read memory, sent it up as binary data.
// Usage: xADDR,LEN
// ADDR and LEN are both base 16.
@@ -3660,6 +3730,7 @@ rnb_err_t RNBRemote::HandlePacket_qSupported(const char *p) {
reply << "memory-tagging+;";
reply << "MultiMemRead+;";
+ reply << "jMultiBreakpoint+;";
return SendPacket(reply.str().c_str());
}
diff --git a/lldb/tools/debugserver/source/RNBRemote.h b/lldb/tools/debugserver/source/RNBRemote.h
index 5e306d889ba1c..2c47ee6bb390e 100644
--- a/lldb/tools/debugserver/source/RNBRemote.h
+++ b/lldb/tools/debugserver/source/RNBRemote.h
@@ -101,6 +101,7 @@ class RNBRemote {
query_process_info, // 'qProcessInfo'
json_query_thread_extended_info, // 'jThreadExtendedInfo'
json_query_get_loaded_dynamic_libraries_infos, // 'jGetLoadedDynamicLibrariesInfos'
+ json_multi_breakpoint, // 'jMultiBreakpoint'
json_query_threads_info, // 'jThreadsInfo'
json_query_get_shared_cache_info, // 'jGetSharedCacheInfo'
pass_signals_to_inferior, // 'QPassSignals'
@@ -190,6 +191,7 @@ class RNBRemote {
rnb_err_t HandlePacket_jThreadExtendedInfo(const char *p);
rnb_err_t HandlePacket_jGetLoadedDynamicLibrariesInfos(const char *p);
rnb_err_t HandlePacket_jThreadsInfo(const char *p);
+ rnb_err_t HandlePacket_jMultiBreakpoint(const char *p);
rnb_err_t HandlePacket_jGetSharedCacheInfo(const char *p);
rnb_err_t HandlePacket_qThreadExtraInfo(const char *p);
rnb_err_t HandlePacket_qThreadStopInfo(const char *p);
>From 422e30fc216e93dbb259cbce16c60e857fe2455e Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Fri, 24 Apr 2026 18:15:40 +0100
Subject: [PATCH 2/3] fixup! address review feedback
---
.../multi-breakpoint/TestMultiBreakpoint.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
index 92bdd0254ec42..cd1a165089e91 100644
--- a/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
+++ b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
@@ -9,12 +9,15 @@
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
+from lldbsuite.test.gdbclientutils import *
@skipUnlessDarwin # Remove once lldbsever support is implemented.
@skipIfOutOfTreeDebugserver
+ at skipIf(archs=no_match(["x86_64", "arm64", "aarch64"]))
class TestMultiBreakpoint(TestBase):
def send_packet(self, packet_str):
+ packet_str = escape_binary(packet_str)
self.runCmd(f"process plugin packet send '{packet_str}'", check=False)
output = self.res.GetOutput()
reply = output.split("\n")
@@ -189,3 +192,12 @@ def make_packet(array):
array = [f"z0,{addr_b},{bp_kind}"]
reply = self.send_packet(make_packet(array))
self.assertMultiResponse(reply, ["error"])
+
+ # --- A failure in the middle should not prevent later requests from succeeding ---
+ array = [
+ f"Z0,{addr_a},{bp_kind}",
+ f"z0,{addr_b},{bp_kind}",
+ f"z0,{addr_a},{bp_kind}",
+ ]
+ reply = self.send_packet(make_packet(array))
+ self.assertMultiResponse(reply, ["OK", "error", "OK"])
>From 77e12753752e344a416bea4cdd13f2936f5a66f9 Mon Sep 17 00:00:00 2001
From: Felipe de Azevedo Piovezan <fpiovezan at apple.com>
Date: Mon, 27 Apr 2026 15:07:15 +0100
Subject: [PATCH 3/3] fixup! add comment on test
---
.../API/functionalities/multi-breakpoint/TestMultiBreakpoint.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
index cd1a165089e91..df35d1cc60adf 100644
--- a/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
+++ b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
@@ -14,6 +14,7 @@
@skipUnlessDarwin # Remove once lldbsever support is implemented.
@skipIfOutOfTreeDebugserver
+#Runs on systems where we can always predict the software break size
@skipIf(archs=no_match(["x86_64", "arm64", "aarch64"]))
class TestMultiBreakpoint(TestBase):
def send_packet(self, packet_str):
More information about the lldb-commits
mailing list