[Lldb-commits] [lldb] 314c655 - [debugserver] Implement MultiBreakpoint (#192914)

via lldb-commits lldb-commits at lists.llvm.org
Mon Apr 27 07:16:24 PDT 2026


Author: Felipe de Azevedo Piovezan
Date: 2026-04-27T14:16:18Z
New Revision: 314c655470b03a8112b2a0107c99a61076852cca

URL: https://github.com/llvm/llvm-project/commit/314c655470b03a8112b2a0107c99a61076852cca
DIFF: https://github.com/llvm/llvm-project/commit/314c655470b03a8112b2a0107c99a61076852cca.diff

LOG: [debugserver] Implement MultiBreakpoint (#192914)

This implements the packet as described in
https://github.com/llvm/llvm-project/pull/192910

The following PRs are related to the MultiBreakpoint feature:

* https://github.com/llvm/llvm-project/pull/192910
* https://github.com/llvm/llvm-project/pull/192914
* https://github.com/llvm/llvm-project/pull/192915
* https://github.com/llvm/llvm-project/pull/192919
* https://github.com/llvm/llvm-project/pull/192962
* https://github.com/llvm/llvm-project/pull/192964
* https://github.com/llvm/llvm-project/pull/192971
* https://github.com/llvm/llvm-project/pull/192988

Added: 
    lldb/test/API/functionalities/multi-breakpoint/Makefile
    lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
    lldb/test/API/functionalities/multi-breakpoint/main.c

Modified: 
    lldb/tools/debugserver/source/JSON.h
    lldb/tools/debugserver/source/RNBRemote.cpp
    lldb/tools/debugserver/source/RNBRemote.h

Removed: 
    


################################################################################
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..a2a078e96bd59
--- /dev/null
+++ b/lldb/test/API/functionalities/multi-breakpoint/TestMultiBreakpoint.py
@@ -0,0 +1,204 @@
+"""
+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
+from lldbsuite.test.gdbclientutils import *
+
+
+ at skipUnlessDarwin  # Remove once lldbsever support is implemented.
+ at skipIfOutOfTreeDebugserver
+# Runs on systems where we can always predict the software break size
+ 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")
+        # 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 
diff erently 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"])
+
+        # --- 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"])

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);


        


More information about the lldb-commits mailing list