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

Reply via email to