From ae9c3221f057b653ec676543194204d763094ba5 Mon Sep 17 00:00:00 2001 From: satya janga Date: Wed, 3 Jun 2026 18:33:55 -0700 Subject: [PATCH] [lldb] Handle accelerator plugin breakpoint actions on the client Wire up the client (ProcessGDBRemote) side of the accelerator plugin breakpoint protocol so the breakpoints requested by lldb-server accelerator plugins are actually set, hit, and acted upon. - GDBRemoteCommunicationClient learns the "accelerator-plugins+" feature from qSupported and gains GetAcceleratorInitializeActions() and AcceleratorBreakpointHit() to drive the jAcceleratorPluginInitialize and jAcceleratorPluginBreakpointHit packets. - ProcessGDBRemote::HandleAcceleratorActions() sets the requested breakpoints as internal breakpoints with a synchronous callback. When a callback fires it resolves any requested symbol values, notifies the plugin, disables the breakpoint and/or auto-resumes the native process per the response, and handles any further actions (e.g. new breakpoints) the plugin returns. Initial actions are fetched in DidLaunchOrAttach. - The mock accelerator plugin now sets its initialize breakpoint on a dedicated "accelerator_initialize" hook (rather than "main") so it only affects this test's inferior, and exercises all three breakpoint types: by name, by name scoped to a shared library, and by address. - Add an end-to-end API test that launches a real process and verifies the breakpoints are set, hit, and chained, plus updated packet-level coverage. --- lldb/docs/resources/lldbgdbremote.md | 32 ++- .../GDBRemoteCommunicationClient.cpp | 79 +++++++ .../gdb-remote/GDBRemoteCommunicationClient.h | 22 ++ .../Process/gdb-remote/ProcessGDBRemote.cpp | 205 ++++++++++++++++++ .../Process/gdb-remote/ProcessGDBRemote.h | 29 +++ .../mock/TestMockAcceleratorBreakpoints.py | 104 +++++++++ .../mock/TestMockAcceleratorPlugin.py | 36 ++- lldb/test/API/accelerator/mock/main.c | 17 +- .../Mock/LLDBServerMockAcceleratorPlugin.cpp | 51 ++++- .../Mock/LLDBServerMockAcceleratorPlugin.h | 8 +- 10 files changed, 562 insertions(+), 21 deletions(-) create mode 100644 lldb/test/API/accelerator/mock/TestMockAcceleratorBreakpoints.py diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md index 1fc04e5ce9e11..ef42ebd6ae41c 100644 --- a/lldb/docs/resources/lldbgdbremote.md +++ b/lldb/docs/resources/lldbgdbremote.md @@ -2706,6 +2706,20 @@ The packets below support debugging hardware accelerators (e.g. GPUs, FPGAs) alongside the native host process via accelerator plugins installed in lldb-server. +An accelerator plugin drives the client by returning a list of +`AcceleratorActions` (currently just breakpoints to set in the native +process). The client can receive `AcceleratorActions` at two points: + +1. Once, in response to the `jAcceleratorPluginInitialize` packet sent when + the native process is launched or attached. +2. In response to a `jAcceleratorPluginBreakpointHit` packet, when a + breakpoint the plugin previously requested is hit. The hit response may + carry a further set of `AcceleratorActions`. + +Each set of `AcceleratorActions` is tagged with a `plugin_name` and an +`identifier` that is unique within that plugin, so the client can ignore a +set it has already processed if the same actions are delivered again. + ### jAcceleratorPluginInitialize This packet requests initialization data from all accelerator plugins @@ -2746,9 +2760,19 @@ If no accelerator plugins are installed, the server does not advertise the `accelerator-plugins+` feature and this packet should not be sent. Each `accelerator_action` may include a `breakpoints` array requesting -breakpoints to be set in the native process. See -`jAcceleratorPluginBreakpointHit` for the callback when those breakpoints -are hit. +breakpoints to be set in the native process. The client sets each of these +as an internal breakpoint and sends a `jAcceleratorPluginBreakpointHit` +packet when one is hit. Each breakpoint object has the following fields: + +| Key | Type | Description | +|----------------|---------|-------------| +| `identifier` | integer | Identifier for this breakpoint, unique within the plugin. It is echoed back in the `jAcceleratorPluginBreakpointHit` packet so the plugin knows which breakpoint was hit. | +| `by_name` | object | Set the breakpoint by function name. Contains `function_name` (string) and an optional `shlib` (string) to scope the breakpoint to a single shared library. | +| `by_address` | object | Set the breakpoint by load address. Contains `load_address` (integer). | +| `symbol_names` | array | Symbol names whose load addresses the client should resolve and deliver in the `jAcceleratorPluginBreakpointHit` packet when this breakpoint is hit. May be empty. | + +Exactly one of `by_name` or `by_address` must be provided for each +breakpoint. In future patches, each `accelerator_action` will include additional fields such as connection info for secondary debug sessions and synchronization @@ -2774,7 +2798,7 @@ The request JSON has the following fields: |-----------------|--------|-------------| | `plugin_name` | string | Name of the plugin that requested the breakpoint. | | `breakpoint` | object | The `AcceleratorBreakpointInfo` that was hit, including its `identifier`. | -| `symbol_values` | array | Array of `{"name": "", "value": }` for each symbol requested in the breakpoint's `symbol_names`. | +| `symbol_values` | array | Array of `{"name": "", "value": }`, one entry for each name in the breakpoint's `symbol_names`. `value` is the resolved load address, or `null` if the client could not find the symbol or convert it to a load address. | The response JSON has the following fields: diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp index 8df7936786b04..391b2e6a78d66 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp @@ -23,6 +23,7 @@ #include "lldb/Target/MemoryRegionInfo.h" #include "lldb/Target/Target.h" #include "lldb/Target/UnixSignals.h" +#include "lldb/Utility/AcceleratorGDBRemotePackets.h" #include "lldb/Utility/Args.h" #include "lldb/Utility/DataBufferHeap.h" #include "lldb/Utility/LLDBAssert.h" @@ -39,6 +40,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/Config/llvm-config.h" // for LLVM_ENABLE_ZLIB +#include "llvm/Support/ErrorExtras.h" #include "llvm/Support/JSON.h" #if HAVE_LIBCOMPRESSION @@ -223,6 +225,79 @@ bool GDBRemoteCommunicationClient::GetMultiBreakpointSupported() { return m_supports_multi_breakpoint == eLazyBoolYes; } +bool GDBRemoteCommunicationClient::GetAcceleratorPluginsSupported() { + if (m_supports_accelerator_plugins == eLazyBoolCalculate) + GetRemoteQSupported(); + return m_supports_accelerator_plugins == eLazyBoolYes; +} + +llvm::Expected> +GDBRemoteCommunicationClient::GetAcceleratorInitializeActions() { + // Get the initial actions (e.g. breakpoints to set) requested by any + // accelerator plugins using the "jAcceleratorPluginInitialize" packet. This + // is sent once when a native process is launched or attached. The empty + // state (no plugins / no actions) is modelled as an empty vector; errors are + // returned to the caller to report. + if (!GetAcceleratorPluginsSupported()) + return std::vector(); + + StringExtractorGDBRemote response; + response.SetResponseValidatorToJSON(); + if (SendPacketAndWaitForResponse("jAcceleratorPluginInitialize", response) != + PacketResult::Success) + return llvm::createStringError( + "failed to send jAcceleratorPluginInitialize packet"); + + if (response.IsUnsupportedResponse()) + return std::vector(); + + if (response.IsErrorResponse()) + return response.GetStatus().takeError(); + + llvm::Expected> actions = + llvm::json::parse>(response.Peek(), + "AcceleratorActions"); + if (actions) + return actions; + + // A bare JSON parse error (e.g. "missing comma at line 4") is meaningless on + // its own, so include both the full response and the parse error; the caller + // logs this and the user can spot the problem in the response. + return llvm::createStringErrorV( + "malformed jAcceleratorPluginInitialize response '{0}': {1}", + response.GetStringRef(), llvm::toString(actions.takeError())); +} + +llvm::Expected +GDBRemoteCommunicationClient::AcceleratorBreakpointHit( + const AcceleratorBreakpointHitArgs &args) { + StreamGDBRemote packet; + packet.PutCString("jAcceleratorPluginBreakpointHit:"); + packet.PutAsJSON(args, /*hex_ascii=*/false); + + StringExtractorGDBRemote response; + if (SendPacketAndWaitForResponse(packet.GetString(), response) != + PacketResult::Success) + return llvm::createStringError( + "failed to send jAcceleratorPluginBreakpointHit packet"); + + if (response.IsErrorResponse()) + return response.GetStatus().takeError(); + + llvm::Expected hit_response = + llvm::json::parse( + response.Peek(), "AcceleratorBreakpointHitResponse"); + if (hit_response) + return hit_response; + + // A bare JSON parse error (e.g. "missing comma at line 4") is meaningless on + // its own, so include both the full response and the parse error; the caller + // logs this and the user can spot the problem in the response. + return llvm::createStringErrorV( + "malformed jAcceleratorPluginBreakpointHit response '{0}': {1}", + response.GetStringRef(), llvm::toString(hit_response.takeError())); +} + bool GDBRemoteCommunicationClient::QueryNoAckModeSupported() { if (m_supports_not_sending_acks == eLazyBoolCalculate) { m_send_acks = true; @@ -321,6 +396,7 @@ void GDBRemoteCommunicationClient::ResetDiscoverableSettings(bool did_exec) { m_x_packet_state.reset(); m_supports_reverse_continue = eLazyBoolCalculate; m_supports_reverse_step = eLazyBoolCalculate; + m_supports_accelerator_plugins = eLazyBoolCalculate; m_supports_qProcessInfoPID = true; m_supports_qfProcessInfo = true; m_supports_qUserName = true; @@ -381,6 +457,7 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_reverse_step = eLazyBoolNo; m_supports_multi_mem_read = eLazyBoolNo; m_supports_multi_breakpoint = eLazyBoolNo; + m_supports_accelerator_plugins = eLazyBoolNo; m_max_packet_size = UINT64_MAX; // It's supposed to always be there, but if // not, we assume no limit @@ -444,6 +521,8 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_multi_mem_read = eLazyBoolYes; else if (x == "jMultiBreakpoint+") m_supports_multi_breakpoint = eLazyBoolYes; + else if (x == "accelerator-plugins+") + m_supports_accelerator_plugins = eLazyBoolYes; // Look for a list of compressions in the features list e.g. // qXfer:features:read+;PacketSize=20000;qEcho+;SupportedCompressions=zlib- // deflate,lzma diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h index 79ca0bcd3ed22..235121d88c9a8 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h @@ -19,6 +19,7 @@ #include #include "lldb/Host/File.h" +#include "lldb/Utility/AcceleratorGDBRemotePackets.h" #include "lldb/Utility/AddressableBits.h" #include "lldb/Utility/ArchSpec.h" #include "lldb/Utility/GDBRemote.h" @@ -346,6 +347,26 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { bool GetMultiBreakpointSupported(); + bool GetAcceleratorPluginsSupported(); + + /// Send the "jAcceleratorPluginInitialize" packet and return the actions + /// requested by each accelerator plugin installed in lldb-server. The packet + /// is only sent if the lldb-server advertised accelerator plugin support via + /// "accelerator-plugins+" in its qSupported response; otherwise (and when no + /// plugin returns actions) this returns an empty vector. Errors are returned + /// for the caller to report. + llvm::Expected> + GetAcceleratorInitializeActions(); + + /// Send the "jAcceleratorPluginBreakpointHit" packet to notify the + /// accelerator plugin that one of its requested breakpoints was hit, and + /// return the plugin's response. This is only used when the lldb-server + /// advertised accelerator plugin support via "accelerator-plugins+" in its + /// qSupported response, since the breakpoints that trigger it are only set in + /// that case. Errors are returned for the caller to report. + llvm::Expected + AcceleratorBreakpointHit(const AcceleratorBreakpointHitArgs &args); + LazyBool SupportsAllocDeallocMemory() // const { // Uncomment this to have lldb pretend the debug server doesn't respond to @@ -582,6 +603,7 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { LazyBool m_supports_reverse_step = eLazyBoolCalculate; LazyBool m_supports_multi_mem_read = eLazyBoolCalculate; LazyBool m_supports_multi_breakpoint = eLazyBoolCalculate; + LazyBool m_supports_accelerator_plugins = eLazyBoolCalculate; bool m_supports_qProcessInfoPID : 1, m_supports_qfProcessInfo : 1, m_supports_qUserName : 1, m_supports_qGroupName : 1, diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp index f6eaf5851338b..c21257113622a 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp @@ -23,6 +23,8 @@ #include #include +#include "lldb/Breakpoint/Breakpoint.h" +#include "lldb/Breakpoint/StoppointCallbackContext.h" #include "lldb/Breakpoint/Watchpoint.h" #include "lldb/Breakpoint/WatchpointAlgorithms.h" #include "lldb/Breakpoint/WatchpointResource.h" @@ -52,6 +54,8 @@ #include "lldb/Interpreter/Options.h" #include "lldb/Interpreter/Property.h" #include "lldb/Symbol/ObjectFile.h" +#include "lldb/Symbol/Symbol.h" +#include "lldb/Symbol/SymbolContext.h" #include "lldb/Target/ABI.h" #include "lldb/Target/DynamicLoader.h" #include "lldb/Target/MemoryRegionInfo.h" @@ -61,7 +65,9 @@ #include "lldb/Target/TargetList.h" #include "lldb/Target/ThreadPlanCallFunction.h" #include "lldb/Utility/Args.h" +#include "lldb/Utility/Baton.h" #include "lldb/Utility/FileSpec.h" +#include "lldb/Utility/FileSpecList.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/State.h" #include "lldb/Utility/StreamString.h" @@ -1060,6 +1066,21 @@ void ProcessGDBRemote::DidLaunchOrAttach(ArchSpec &process_arch) { else SetUnixSignals(UnixSignals::Create(GetTarget().GetArchitecture())); } + + // Ask any accelerator plugins installed in lldb-server for their initial + // actions (e.g. breakpoints to set in the native process). + llvm::Expected> init_actions = + m_gdb_comm.GetAcceleratorInitializeActions(); + if (!init_actions) { + LLDB_LOG_ERROR(log, init_actions.takeError(), + "failed to get accelerator initialize actions: {0}"); + } else { + for (const AcceleratorActions &actions : *init_actions) { + if (llvm::Error error = HandleAcceleratorActions(actions)) + LLDB_LOG_ERROR(log, std::move(error), + "failed to handle accelerator actions: {0}"); + } + } } void ProcessGDBRemote::LoadStubBinaries() { @@ -4124,6 +4145,190 @@ bool ProcessGDBRemote::NewThreadNotifyBreakpointHit( return false; } +namespace { +/// Baton that carries the breakpoint hit arguments to the accelerator plugin +/// breakpoint callback. +class AcceleratorBreakpointCallbackBaton + : public TypedBaton { +public: + explicit AcceleratorBreakpointCallbackBaton( + std::unique_ptr data) + : TypedBaton(std::move(data)) {} +}; +} // namespace + +llvm::Error +ProcessGDBRemote::HandleAcceleratorActions(const AcceleratorActions &actions) { + Log *log = GetLog(GDBRLog::Process); + + // The same set of actions can be delivered to the client more than once: a + // plugin may keep reporting the same actions (with the same identifier) on + // subsequent native stops until its state advances. The identifier uniquely + // names a set of actions for a plugin, so skip any set we have already + // processed to avoid re-running its side effects (e.g. setting the same + // breakpoints again). + auto it = m_processed_accelerator_actions.find(actions.plugin_name); + if (it != m_processed_accelerator_actions.end() && + it->second == actions.identifier) { + LLDB_LOG(log, + "ProcessGDBRemote::HandleAcceleratorActions skipping already " + "processed actions for plugin '{0}' with identifier {1}", + actions.plugin_name, actions.identifier); + return llvm::Error::success(); + } + m_processed_accelerator_actions[actions.plugin_name] = actions.identifier; + + // Handle each kind of action. More action kinds will be handled here in the + // future, so only return early on error; otherwise fall through so the next + // kind of action still gets a chance to run. + if (!actions.breakpoints.empty()) { + if (llvm::Error error = HandleAcceleratorBreakpoints(actions)) + return error; + } + + return llvm::Error::success(); +} + +llvm::Error ProcessGDBRemote::HandleAcceleratorBreakpoints( + const AcceleratorActions &actions) { + Target &target = GetTarget(); + llvm::Error error = llvm::Error::success(); + for (const AcceleratorBreakpointInfo &bp : actions.breakpoints) { + // Carry data with the breakpoint so the callback can notify the plugin + // when the breakpoint is hit. + auto args_up = std::make_unique(); + args_up->plugin_name = actions.plugin_name; + args_up->breakpoint = bp; + + // Each breakpoint must specify exactly one of by_name or by_address. Bad + // breakpoints are collected as errors but don't stop the remaining ones + // from being set. + BreakpointSP bp_sp; + if (bp.by_name && bp.by_address) { + error = llvm::joinErrors( + std::move(error), + llvm::createStringErrorV( + "accelerator breakpoint {0} specifies both a by_name and a " + "by_address specification", + bp.identifier)); + continue; + } else if (bp.by_name) { + FileSpecList bp_modules; + if (bp.by_name->shlib && !bp.by_name->shlib->empty()) + bp_modules.Append(FileSpec(*bp.by_name->shlib)); + bp_sp = target.CreateBreakpoint( + bp_modules.GetSize() ? &bp_modules : nullptr, // Containing modules. + nullptr, // Containing source. + bp.by_name->function_name.c_str(), // Function name. + eFunctionNameTypeFull, // Function name type. + eLanguageTypeUnknown, // Language type. + 0, // Byte offset. + false, // Offset is insn count. + eLazyBoolNo, // Skip prologue. + true, // Internal breakpoint. + false); // Request hardware. + } else if (bp.by_address) { + bp_sp = target.CreateBreakpoint(bp.by_address->load_address, + /*internal=*/true, + /*request_hardware=*/false); + } else { + error = llvm::joinErrors( + std::move(error), + llvm::createStringErrorV( + "accelerator breakpoint {0} has neither a by_name nor a " + "by_address specification", + bp.identifier)); + continue; + } + + if (!bp_sp) { + error = llvm::joinErrors( + std::move(error), + llvm::createStringErrorV("failed to set accelerator breakpoint {0}", + bp.identifier)); + continue; + } + + // Give the internal breakpoint a meaningful description for stop reasons, + // including the plugin that requested it. + std::string kind = + llvm::formatv("accelerator-plugin ({0})", actions.plugin_name); + bp_sp->SetBreakpointKind(kind.c_str()); + auto baton_sp = std::make_shared( + std::move(args_up)); + bp_sp->SetCallback(AcceleratorBreakpointHitCallback, baton_sp, + /*is_synchronous=*/true); + } + return error; +} + +bool ProcessGDBRemote::AcceleratorBreakpointHitCallback( + void *baton, StoppointCallbackContext *context, lldb::user_id_t break_id, + lldb::user_id_t break_loc_id) { + ProcessSP process_sp = context->exe_ctx_ref.GetProcessSP(); + ProcessGDBRemote *process = static_cast(process_sp.get()); + return process->AcceleratorBreakpointHit(baton, context, break_id, + break_loc_id); +} + +bool ProcessGDBRemote::AcceleratorBreakpointHit( + void *baton, StoppointCallbackContext *context, lldb::user_id_t break_id, + lldb::user_id_t break_loc_id) { + AcceleratorBreakpointHitArgs *callback_data = + static_cast(baton); + // Copy the args so we can fill in requested symbol values before notifying + // lldb-server. + AcceleratorBreakpointHitArgs args = *callback_data; + Target &target = GetTarget(); + + const std::vector &symbol_names = args.breakpoint.symbol_names; + args.symbol_values.resize(symbol_names.size()); + for (size_t i = 0; i < symbol_names.size(); ++i) { + args.symbol_values[i].name = symbol_names[i]; + SymbolContextList sc_list; + target.GetImages().FindSymbolsWithNameAndType(ConstString(symbol_names[i]), + eSymbolTypeAny, sc_list); + for (const SymbolContext &sc : sc_list) { + if (!sc.symbol) + continue; + addr_t load_addr = sc.symbol->GetAddress().GetLoadAddress(&target); + if (load_addr != LLDB_INVALID_ADDRESS) { + args.symbol_values[i].value = load_addr; + break; + } + } + } + + Log *log = GetLog(GDBRLog::Process); + llvm::Expected response = + m_gdb_comm.AcceleratorBreakpointHit(args); + if (!response) { + LLDB_LOG_ERROR(log, response.takeError(), + "accelerator breakpoint hit notification failed: {0}"); + // We could not reach the plugin, so auto-resume rather than stopping the + // native process at an internal breakpoint the user can't see. + return false; + } + + // Disable the breakpoint if requested, but keep it around so its hit count + // and other stats remain visible. + if (response->disable_bp) { + if (BreakpointSP bp_sp = target.GetBreakpointByID(break_id)) + bp_sp->SetEnabled(false); + } + + // The plugin may request new actions (e.g. additional breakpoints) in + // response to this breakpoint being hit. + if (response->actions) { + if (llvm::Error error = HandleAcceleratorActions(*response->actions)) + LLDB_LOG_ERROR(log, std::move(error), + "failed to handle accelerator actions: {0}"); + } + + // Returning true stops the native process; false auto-resumes it. + return !response->auto_resume_native; +} + Status ProcessGDBRemote::UpdateAutomaticSignalFiltering() { Log *log = GetLog(GDBRLog::Process); LLDB_LOG(log, "Check if need to update ignored signals"); diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h index c6d67b3aa5350..5039a0ab74eaa 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h @@ -478,6 +478,35 @@ class ProcessGDBRemote : public Process, /// Remove the breakpoints associated with thread creation from the Target. void RemoveNewThreadBreakpoints(); + /// Handle a set of actions requested by an accelerator plugin. Currently this + /// only sets the breakpoints requested in \a actions. + llvm::Error HandleAcceleratorActions(const AcceleratorActions &actions); + + /// Set the breakpoints requested by an accelerator plugin as internal + /// breakpoints with a callback that notifies the plugin when they are hit. + /// Returns an error if any breakpoint could not be set; the remaining + /// breakpoints are still set. + llvm::Error HandleAcceleratorBreakpoints(const AcceleratorActions &actions); + + /// Breakpoint callback invoked when an accelerator-plugin-requested + /// breakpoint is hit. Resolves any requested symbol values, notifies the + /// plugin via the "jAcceleratorPluginBreakpointHit" packet, and handles the + /// response. + static bool AcceleratorBreakpointHitCallback( + void *baton, StoppointCallbackContext *context, lldb::user_id_t break_id, + lldb::user_id_t break_loc_id); + + bool AcceleratorBreakpointHit(void *baton, StoppointCallbackContext *context, + lldb::user_id_t break_id, + lldb::user_id_t break_loc_id); + + /// Tracks the last action identifier handled per accelerator plugin so the + /// same actions are not processed more than once. Accelerator actions can + /// arrive in a stop reply packet, and if the debugger re-requests the stop + /// reply with a '?' packet we can be handed the same actions again; this + /// guards against handling them (e.g. setting the same breakpoints) twice. + std::map m_processed_accelerator_actions; + // ContinueDelegate interface void HandleAsyncStdout(llvm::StringRef out) override; void HandleAsyncMisc(llvm::StringRef data) override; diff --git a/lldb/test/API/accelerator/mock/TestMockAcceleratorBreakpoints.py b/lldb/test/API/accelerator/mock/TestMockAcceleratorBreakpoints.py new file mode 100644 index 0000000000000..885e73e3cec15 --- /dev/null +++ b/lldb/test/API/accelerator/mock/TestMockAcceleratorBreakpoints.py @@ -0,0 +1,104 @@ +""" +End-to-end test for accelerator plugin breakpoints. + +Launches a real process against an lldb-server that has the mock accelerator +plugin enabled and verifies that the breakpoints requested by the plugin are +set in the native process, hit, and that hitting one breakpoint can request +further breakpoints. This exercises all three breakpoint types: by name, by +name scoped to a shared library, and by address. +""" + +import lldb +import lldbsuite.test.lldbutil as lldbutil +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import configuration + + +def uint64_to_int64(value): + """Reinterpret an unsigned 64-bit value as a signed 64-bit integer.""" + if value >= (1 << 63): + return value - (1 << 64) + return value + + +class MockAcceleratorBreakpointsTestCase(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def setUp(self): + super().setUp() + if "mock-accelerator" not in configuration.enabled_plugins: + self.skipTest("mock-accelerator plugin is not enabled") + + def check_accelerator_breakpoint_stop(self, process, function_name, hit_count=None): + """Verify the process stopped at an internal accelerator breakpoint in + the given function. If hit_count is not None, also verify the + breakpoint's hit count. Returns the breakpoint.""" + self.assertState(process.GetState(), lldb.eStateStopped) + thread = process.GetSelectedThread() + + # The stop must be due to a breakpoint, and the frame must be in the + # expected function. + self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) + frame = thread.GetFrameAtIndex(0) + self.assertEqual(frame.GetFunctionName(), function_name) + + # The breakpoint id is carried in the stop reason data. Accelerator + # breakpoints are internal, so they are not in the public breakpoint + # list, but can still be looked up by id. The datum is an unsigned + # 64-bit value holding the (signed) breakpoint id; internal ids are + # negative. + self.assertGreater(thread.GetStopReasonDataCount(), 0) + bp_id = uint64_to_int64(thread.GetStopReasonDataAtIndex(0)) + bp = process.GetTarget().FindBreakpointByID(bp_id) + self.assertTrue(bp.IsValid()) + self.assertTrue(bp.IsInternal(), "accelerator breakpoints are internal") + + if hit_count is not None: + self.assertEqual(bp.GetHitCount(), hit_count) + return bp + + @skipIfRemote + @add_test_categories(["llgs"]) + def test_accelerator_breakpoints(self): + """The mock accelerator plugin drives breakpoints in the inferior.""" + self.build() + exe = self.getBuildArtifact("a.out") + target = self.dbg.CreateTarget(exe) + self.assertTrue(target, VALID_TARGET) + + # Launching the process should stop at the + # "mock_gpu_accelerator_initialize" breakpoint that the mock plugin + # requested via jAcceleratorPluginInitialize (it requests the native + # process not auto-resume). This is a breakpoint by name with no shared + # library. + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + self.assertTrue(process, PROCESS_IS_VALID) + self.check_accelerator_breakpoint_stop( + process, "mock_gpu_accelerator_initialize", hit_count=1 + ) + + # The accelerator breakpoint was set and hit, yet it is internal, so it + # never appears in the public breakpoint list. + self.assertEqual(target.GetNumBreakpoints(), 0) + + # Hitting the mock_gpu_accelerator_initialize breakpoint caused the + # plugin to request two more breakpoints: one by address (on + # "mock_gpu_accelerator_compute", from the symbol value delivered with + # the hit) and one by name scoped to the "a.out" shared library (on + # "mock_gpu_accelerator_finish"). main() calls + # mock_gpu_accelerator_compute() first. + process.Continue() + self.check_accelerator_breakpoint_stop( + process, "mock_gpu_accelerator_compute", hit_count=1 + ) + + process.Continue() + self.check_accelerator_breakpoint_stop( + process, "mock_gpu_accelerator_finish", hit_count=1 + ) + + # No more accelerator breakpoints; the process runs to exit. + process.Continue() + self.assertState(process.GetState(), lldb.eStateExited) + self.assertEqual(process.GetExitStatus(), 0) diff --git a/lldb/test/API/accelerator/mock/TestMockAcceleratorPlugin.py b/lldb/test/API/accelerator/mock/TestMockAcceleratorPlugin.py index 8e2bd0d604366..1eeeb7f731eb7 100644 --- a/lldb/test/API/accelerator/mock/TestMockAcceleratorPlugin.py +++ b/lldb/test/API/accelerator/mock/TestMockAcceleratorPlugin.py @@ -81,7 +81,12 @@ def test_jAcceleratorPluginInitialize_returns_breakpoints(self): bp = breakpoints[0] self.assertIn("identifier", bp) self.assertIn("by_name", bp) - self.assertEqual(bp["by_name"]["function_name"], "main") + self.assertEqual( + bp["by_name"]["function_name"], "mock_gpu_accelerator_initialize" + ) + # The initialize breakpoint requests the "mock_gpu_accelerator_compute" + # symbol value. + self.assertEqual(bp["symbol_names"], ["mock_gpu_accelerator_compute"]) @add_test_categories(["llgs"]) def test_jAcceleratorPluginBreakpointHit_response(self): @@ -92,13 +97,18 @@ def test_jAcceleratorPluginBreakpointHit_response(self): self.add_qSupported_packets() self.expect_gdbremote_sequence() + # Simulate the initialize breakpoint being hit, supplying the requested + # "mock_gpu_accelerator_compute" symbol value so the plugin sets a + # breakpoint by address. hit_args = { "plugin_name": "mock", "breakpoint": { "identifier": 1, - "symbol_names": [], + "symbol_names": ["mock_gpu_accelerator_compute"], }, - "symbol_values": [], + "symbol_values": [ + {"name": "mock_gpu_accelerator_compute", "value": 0x4000} + ], } hit_json = json.dumps(hit_args, separators=(",", ":")) escaped_json = escape_binary(hit_json) @@ -111,12 +121,22 @@ def test_jAcceleratorPluginBreakpointHit_response(self): self.assertIn("auto_resume_native", response) self.assertFalse(response["auto_resume_native"]) - # Verify that the response includes new actions with a breakpoint. + # Verify the response includes new actions with both a by-name (scoped + # to a shared library) and a by-address breakpoint. self.assertIn("actions", response) actions = response["actions"] self.assertEqual(actions["plugin_name"], "mock") self.assertIn("breakpoints", actions) - new_bps = actions["breakpoints"] - self.assertGreater(len(new_bps), 0) - self.assertEqual(new_bps[0]["identifier"], 2) - self.assertEqual(new_bps[0]["by_name"]["function_name"], "exit") + new_bps = {bp["identifier"]: bp for bp in actions["breakpoints"]} + + # Breakpoint by name scoped to a shared library. + self.assertIn(3, new_bps) + self.assertEqual( + new_bps[3]["by_name"]["function_name"], "mock_gpu_accelerator_finish" + ) + self.assertEqual(new_bps[3]["by_name"]["shlib"], "a.out") + + # Breakpoint by address, using the supplied + # "mock_gpu_accelerator_compute" symbol value. + self.assertIn(2, new_bps) + self.assertEqual(new_bps[2]["by_address"]["load_address"], 0x4000) diff --git a/lldb/test/API/accelerator/mock/main.c b/lldb/test/API/accelerator/mock/main.c index 78f2de106c92b..5f4c7b1fd3147 100644 --- a/lldb/test/API/accelerator/mock/main.c +++ b/lldb/test/API/accelerator/mock/main.c @@ -1 +1,16 @@ -int main(void) { return 0; } +// The mock accelerator plugin sets its initialize breakpoint on this function. +// Using a dedicated, uniquely named function (rather than "main") ensures the +// mock plugin only affects this test program and not other inferiors launched +// by lldb-server. +void mock_gpu_accelerator_initialize(void) {} + +int mock_gpu_accelerator_compute(int x) { return x * 2; } + +int mock_gpu_accelerator_finish(void) { return 0; } + +int main(void) { + mock_gpu_accelerator_initialize(); + int result = mock_gpu_accelerator_compute(21); + mock_gpu_accelerator_finish(); + return result == 42 ? 0 : 1; +} diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp index cb2951919537a..f89fb90e26aa6 100644 --- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp +++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp @@ -23,9 +23,17 @@ std::optional LLDBServerMockAcceleratorPlugin::GetInitializeActions() { AcceleratorActions actions(GetPluginName(), 1); + // Set a breakpoint by function name (no shared library scope) on the + // dedicated "mock_gpu_accelerator_initialize" hook and ask for the load + // address of "mock_gpu_accelerator_compute" to be delivered when it is hit. + // Using a dedicated, uniquely named function (rather than "main") keeps this + // mock from affecting other inferiors that lldb-server launches when the + // plugin is compiled in. AcceleratorBreakpointInfo bp; bp.identifier = kBreakpointIDInitialize; - bp.by_name = AcceleratorBreakpointByName{std::nullopt, "main"}; + bp.by_name = AcceleratorBreakpointByName{std::nullopt, + "mock_gpu_accelerator_initialize"}; + bp.symbol_names.push_back("mock_gpu_accelerator_compute"); actions.breakpoints.push_back(std::move(bp)); return actions; @@ -35,16 +43,45 @@ llvm::Expected LLDBServerMockAcceleratorPlugin::BreakpointWasHit( AcceleratorBreakpointHitArgs &args) { AcceleratorBreakpointHitResponse response; - if (args.breakpoint.identifier == kBreakpointIDInitialize) { + + switch (args.breakpoint.identifier) { + case kBreakpointIDInitialize: { + // The initialize breakpoint was hit. Disable it, stop the native process, + // and request two more breakpoints to exercise the remaining breakpoint + // types. response.disable_bp = true; response.auto_resume_native = false; - AcceleratorActions actions(GetPluginName(), kBreakpointIDExit); - AcceleratorBreakpointInfo bp; - bp.identifier = kBreakpointIDExit; - bp.by_name = AcceleratorBreakpointByName{std::nullopt, "exit"}; - actions.breakpoints.push_back(std::move(bp)); + AcceleratorActions actions(GetPluginName(), 2); + + // Breakpoint by function name scoped to a shared library. Tests build to + // "a.out", so use that as the shared library name. + AcceleratorBreakpointInfo by_name_shlib; + by_name_shlib.identifier = kBreakpointIDByNameShlib; + by_name_shlib.by_name = + AcceleratorBreakpointByName{"a.out", "mock_gpu_accelerator_finish"}; + actions.breakpoints.push_back(std::move(by_name_shlib)); + + // Breakpoint by address, using the "mock_gpu_accelerator_compute" symbol + // value that was delivered with this breakpoint hit. + if (std::optional compute_addr = + args.GetSymbolValue("mock_gpu_accelerator_compute")) { + AcceleratorBreakpointInfo by_address; + by_address.identifier = kBreakpointIDByAddress; + by_address.by_address = AcceleratorBreakpointByAddress{*compute_addr}; + actions.breakpoints.push_back(std::move(by_address)); + } + response.actions = std::move(actions); + break; } + case kBreakpointIDByAddress: + case kBreakpointIDByNameShlib: + // Disable and stop the native process so the hit is observable. + response.disable_bp = true; + response.auto_resume_native = false; + break; + } + return response; } diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h index 1d3d03121c02e..7a1b57e0bb0b7 100644 --- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h +++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h @@ -24,8 +24,14 @@ class LLDBServerMockAcceleratorPlugin : public LLDBServerAcceleratorPlugin { BreakpointWasHit(AcceleratorBreakpointHitArgs &args) override; private: + // Breakpoint set during initialization, by function name with no shared + // library. Requests the "compute" symbol value when hit. static constexpr int64_t kBreakpointIDInitialize = 1; - static constexpr int64_t kBreakpointIDExit = 2; + // Breakpoint set by address, using the "compute" symbol value delivered when + // the initialize breakpoint was hit. + static constexpr int64_t kBreakpointIDByAddress = 2; + // Breakpoint set by function name scoped to a shared library. + static constexpr int64_t kBreakpointIDByNameShlib = 3; }; } // namespace lldb_server