Files
Pavel Labath 26cae628b4 Reapply "[lldb] Do not refcount breakpoints in lldb-server" (#195858) (#196891)
This reapplies #195858 with a fix for 32-bit arm (and generally, any
architecture that uses software single-stepping). The problem was that
the temporary breakpoints used for single-stepping were interfering with
the breakpoints set by the client.

The fix is to check for existing breakpoints before setting the
temporary ones. To achieve this, I've separated the notion of "next PC
candidates for a thread" from "step breakpoints we've actually set".

The freebsd code had some software single stepping code, but:
- this was [introduced](https://reviews.llvm.org/D95802) for mips64
support, which was
[removed](https://github.com/llvm/llvm-project/pull/179582) earlier this
year
- AFAICT, this never worked since the original patch only checked
`m_threads_stepping_with_breakpoint`, but never set it to anything.

This is why I'm removing the remnants of the single step support instead
of trying to adapt it.


The original commit message was:

We did not say so explictly, but I'd argue that via
https://github.com/llvm/llvm-project/pull/195815, we are supporting
stubs which do not refcount breakpoints. In these stubs the set/clear
breakpoint packets are idempotent:

-    setting a breakpoint for the second time is a no-op (returns OK)
- clearing a breakpoint clears it, regardless of how many times it has
been set
- clearing a non-existent breakpoint (either because it was already
cleared, or because it was never set) returns an error

This makes lldb-server one of those stubs, which makes the code slightly
simpler, but more importantly, ensures we do not regress this behavior.
2026-05-12 13:00:33 +02:00

197 lines
7.4 KiB
Python

"""
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 *
@skipIfWindows # No server on Windows.
@skipIfOutOfTreeDebugserver
# Runs on systems where we can always predict the software break size
@skipIf(archs=no_match(["x86_64", "arm64", "aarch64"]))
class TestMultiBreakpoint(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def check_invalid_packet(self, packet_str):
reply = lldbutil.send_packet_get_reply(self, packet_str)
if reply.startswith("E"):
return
else:
self.assertMultiResponse(reply, ["error"])
def send_packet(self, packet_str):
return lldbutil.send_packet_get_reply(self, packet_str)
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):
# Debugserver uses refcounted breakpoints
breakpoints_are_refcounted = self.platformIsDarwin()
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.
capabilities = lldbutil.get_qsupported_capabilities(self)
self.assertIn("jMultiBreakpoint+", capabilities)
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" if breakpoints_are_refcounted else "error"]
)
# --- 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" if breakpoints_are_refcounted else "error", "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"])