https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/213176
>From b7e2d1ec547ab6311c74967fcb779389fd359d76 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Thu, 30 Jul 2026 17:00:49 -0700 Subject: [PATCH 1/2] [lldb] Scope Wasm global reads to a module instance A Wasm global belongs to a module instance, not to a frame. The global index space is per instance, and DW_OP_WASM_location's global operand indexes the index space of the instance whose code is being evaluated, so two instances of one module have separate globals. qWasmGlobal takes a frame index instead, which leaves a global of an instance with no active frame out of reach. Name the instance instead: qWasmGlobal:<index>;instance:<id>;. The id is the module id LLDB already carries in bits 61:32 of a Wasm address, so nothing has to be enumerated first. A stub opts in by advertising qWasmInstance+ in its qSupported response, which covers every Wasm packet whose scope is an instance rather than a frame, so a query for other instance-scoped state does not need a packet of its own. A stub that does not advertise it keeps getting the frame form. Linear memory needs no suffix: a Wasm address already carries the id of the instance it points into, which the documentation now spells out along with the object space tag a reported load address has to carry. Every value the id field can hold names an instance, zero included, so the sentinel for no instance lives outside the 30-bit id range. Naming an address space explicitly, as #206370 proposes, is a separate feature and stays out of this packet until it lands. Fixes #212833 --- lldb/docs/resources/lldbgdbremote.md | 61 +- .../Plugins/ObjectFile/wasm/WasmAddress.h | 16 + .../GDBRemoteCommunicationClient.cpp | 10 + .../gdb-remote/GDBRemoteCommunicationClient.h | 6 + .../Plugins/Process/wasm/ProcessWasm.cpp | 119 ++- .../source/Plugins/Process/wasm/ProcessWasm.h | 28 +- .../Process/wasm/RegisterContextWasm.cpp | 14 +- .../Process/wasm/RegisterContextWasm.h | 7 + .../Plugins/Process/wasm/UnwindWasm.cpp | 6 +- .../gdb_remote_client/TestWasm.py | 691 +++++++++++++++--- .../simple_global_frame_base.yaml | 238 ++++++ 11 files changed, 1064 insertions(+), 132 deletions(-) create mode 100644 lldb/test/API/functionalities/gdb_remote_client/simple_global_frame_base.yaml diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md index 93090c19c5ec0..bc39292427c45 100644 --- a/lldb/docs/resources/lldbgdbremote.md +++ b/lldb/docs/resources/lldbgdbremote.md @@ -2638,6 +2638,20 @@ The packet below are supported by the [WAMR](https://github.com/bytecodealliance/wasm-micro-runtime) and [V8](https://v8.dev) Wasm runtimes. +An address is 64 bits wide: an address space tag in bits 63:62, the id of the +module instance the address belongs to in bits 61:32, and a 32-bit offset into +that space. The tag is 0 for linear memory and 1 for the object space, which +holds the module image, so bit 63 is always clear on the wire. A stub therefore +reports the load address of an instance in `qXfer:libraries:read` as +`(1 << 62) | (<instance id> << 32)`, the base of its module in the object space, +and the same id appears in the PCs returned by `qWasmCallStack`. An id is unique +among live instances, and zero is an id like any other. LLDB keys a module on +the name it is reported under, so each instance needs a name of its own. + +An address the running code computed, such as one relative to a frame base, +carries no id, and a stub serves it from the instance the current thread is +executing. + ### qWasmCallStack @@ -2657,19 +2671,58 @@ stack traces. ### qWasmGlobal -Get the value of a Wasm global variable for the given frame index at the given -variable index. The indexes are encoded as base 10. The result is a hex-encoded -little-endian value of the global. +Get the value of a Wasm global variable at the given variable index. The indexes +are encoded as base 10. The result is a hex-encoded little-endian value of the +whole global, or `E<nn>`. + +A global index space belongs to a module instance, so an index only names a +global together with the instance to read it from. A stub that advertises +`qWasmInstance+` is named that instance directly: ``` -send packet: $qWasmGlobal:0;2#cb +send packet: $qWasmGlobal:2;instance:16;#32 read packet: $e0030100#b9 ``` +A stub that does not is given a frame index instead, which only reaches the +instance that frame is executing: + +``` +send packet: $qWasmGlobal:0;2#31 +read packet: $e0030100#b9 +``` + +The first field is a global index in the first form and a frame index in the +second, so a stub tells the two apart by the `<key>:<value>` suffix. An +unrecognized instance id must be answered with an error rather than with another +instance's global. + **Priority to Implement:** Only required for Wasm support. Necessary to show variables. +### qWasmInstance (qSupported feature) + +A stub advertises `qWasmInstance+` when a query may name the module instance it +is about, rather than only the instance some frame is executing. LLDB needs this +to read a global of an instance with no frame on the stack, which it finds by +name in the debug info of a module. + +``` +send packet: qSupported:xmlRegisters=i386,arm,mips +read packet: qXfer:libraries:read+;qWasmInstance+;PacketSize=1000 +``` + +An instance is named with a `;instance:<id>;` suffix in place of a frame index, +in which the id is encoded as base 10. `qWasmGlobal` is the only packet that +carries it today, because a global is the only Wasm state with no address to +identify its instance. A later query for instance-scoped state carries the same +suffix rather than adding a packet of its own. + +**Priority to Implement:** Only required for Wasm support. Necessary to show the +globals of a module instance that has no active frame. + + ### qWasmLocal Get the value of a Wasm function argument or local variable for the given frame diff --git a/lldb/source/Plugins/ObjectFile/wasm/WasmAddress.h b/lldb/source/Plugins/ObjectFile/wasm/WasmAddress.h index 8c731de38e36f..17916857580b0 100644 --- a/lldb/source/Plugins/ObjectFile/wasm/WasmAddress.h +++ b/lldb/source/Plugins/ObjectFile/wasm/WasmAddress.h @@ -9,6 +9,7 @@ #ifndef LLDB_SOURCE_PLUGINS_OBJECTFILE_WASM_WASMADDRESS_H #define LLDB_SOURCE_PLUGINS_OBJECTFILE_WASM_WASMADDRESS_H +#include "lldb/lldb-defines.h" #include "lldb/lldb-types.h" #include <cstdint> @@ -56,6 +57,13 @@ static constexpr uint64_t kWasmModuleIDMask = static constexpr uint64_t kWasmAddressTypeMask = MakeFieldMask(kWasmAddressTypeBits, kWasmAddressTypeShift); +/// A value that names no module. Every value the id field can hold names a +/// module, zero included, so the sentinel has to come from outside that range. +static constexpr uint32_t kWasmInvalidModuleID = UINT32_MAX; + +static_assert(kWasmInvalidModuleID > (kWasmModuleIDMask >> kWasmModuleIDShift), + "the sentinel has to fall outside the range of a module id"); + /// For the purpose of debugging, we can represent all these separated 32-bit /// address spaces with a single virtual 64-bit address space. The /// wasm_addr_t provides this encoding using bitfields. @@ -81,6 +89,14 @@ struct wasm_addr_t { static_assert(sizeof(wasm_addr_t) == 8, ""); +/// The module an address belongs to, or kWasmInvalidModuleID for an invalid +/// address. +inline uint32_t GetWasmModuleID(lldb::addr_t addr) { + if (addr == LLDB_INVALID_ADDRESS) + return kWasmInvalidModuleID; + return wasm_addr_t(addr).GetModuleID(); +} + } // namespace wasm } // namespace lldb_private diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp index b440869f25984..1397abf02d7d6 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp @@ -233,6 +233,12 @@ bool GDBRemoteCommunicationClient::GetAcceleratorPluginsSupported() { return m_supports_accelerator_plugins == eLazyBoolYes; } +bool GDBRemoteCommunicationClient::GetWasmInstanceSupported() { + if (m_supports_wasm_instance == eLazyBoolCalculate) + GetRemoteQSupported(); + return m_supports_wasm_instance == eLazyBoolYes; +} + llvm::Expected<std::vector<AcceleratorActions>> GDBRemoteCommunicationClient::GetAcceleratorInitializeActions() { // Get the initial actions (e.g. breakpoints to set) requested by any @@ -431,6 +437,7 @@ void GDBRemoteCommunicationClient::ResetDiscoverableSettings(bool did_exec) { m_supports_jModulesInfo = true; m_supports_multi_mem_read = eLazyBoolCalculate; m_supports_multi_breakpoint = eLazyBoolCalculate; + m_supports_wasm_instance = eLazyBoolCalculate; } // These flags should be reset when we first connect to a GDB server and when @@ -460,6 +467,7 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_multi_mem_read = eLazyBoolNo; m_supports_multi_breakpoint = eLazyBoolNo; m_supports_accelerator_plugins = eLazyBoolNo; + m_supports_wasm_instance = eLazyBoolNo; m_max_packet_size = UINT64_MAX; // It's supposed to always be there, but if // not, we assume no limit @@ -527,6 +535,8 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_multi_breakpoint = eLazyBoolYes; else if (x == "accelerator-plugins+") m_supports_accelerator_plugins = eLazyBoolYes; + else if (x == "qWasmInstance+") + m_supports_wasm_instance = 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 3a0a34f840c21..cc16949eef000 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h @@ -359,6 +359,11 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { bool GetAcceleratorPluginsSupported(); + /// Whether the WebAssembly stub can be told which module instance to read + /// from, which it advertises with "qWasmInstance+" in its qSupported + /// response. + bool GetWasmInstanceSupported(); + /// 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 @@ -614,6 +619,7 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { LazyBool m_supports_multi_mem_read = eLazyBoolCalculate; LazyBool m_supports_multi_breakpoint = eLazyBoolCalculate; LazyBool m_supports_accelerator_plugins = eLazyBoolCalculate; + LazyBool m_supports_wasm_instance = 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/wasm/ProcessWasm.cpp b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp index bf454b2ca4476..2c627548c8aaf 100644 --- a/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp +++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp @@ -13,6 +13,7 @@ #include "lldb/Core/Value.h" #include "lldb/Target/StackFrame.h" #include "lldb/Utility/DataBufferHeap.h" +#include "llvm/Support/ErrorExtras.h" #include <cstring> #include "lldb/Target/UnixSignals.h" @@ -90,24 +91,14 @@ std::shared_ptr<ThreadGDBRemote> ProcessWasm::CreateThread(lldb::tid_t tid) { size_t ProcessWasm::ReadGlobal(uint32_t module_id, uint32_t index, void *buf, size_t size, Status &error) { - // FIXME: The module id is what should select the instance holding the global, - // but the qWasmGlobal packet takes a frame index instead, so the selected - // frame has to stand in for the instance. That leaves a global in an instance - // with no active frame out of reach. See - // https://github.com/llvm/llvm-project/issues/212833. - ThreadSP thread = GetThreadList().GetSelectedThread(); - StackFrameSP frame = - thread ? thread->GetSelectedFrame(DoNoSelectMostRelevantFrame) : nullptr; - if (!frame) { - error = Status::FromErrorStringWithFormatv( - "Wasm global read failed: no frame to read global {0} of module {1:x} " - "from", - index, module_id); - return 0; - } + // Looking for a frame drives the unwinder, so only pay for it when the read + // has to go through one. + const uint32_t frame_index = CanNameInstance(module_id) + ? LLDB_INVALID_INDEX32 + : GetFallbackFrameIndex(module_id); llvm::Expected<lldb::DataBufferSP> buffer = - GetWasmVariable(eWasmTagGlobal, frame->GetConcreteFrameIndex(), index); + GetWasmGlobal(module_id, index, frame_index); if (!buffer) { error = Status::FromError(buffer.takeError()); return 0; @@ -127,8 +118,28 @@ size_t ProcessWasm::ReadGlobal(uint32_t module_id, uint32_t index, void *buf, return size; } +uint32_t ProcessWasm::GetFallbackFrameIndex(uint32_t module_id) { + ThreadSP thread = GetThreadList().GetSelectedThread(); + StackFrameSP frame = + thread ? thread->GetSelectedFrame(DoNoSelectMostRelevantFrame) : nullptr; + if (!frame) + return LLDB_INVALID_INDEX32; + + // A frame can only stand in for the module the stub reports it executing. + const uint32_t frame_index = frame->GetConcreteFrameIndex(); + ThreadWasm &wasm_thread = static_cast<ThreadWasm &>(*thread); + if (GetWasmModuleID(wasm_thread.GetConcreteFramePC(frame_index)) != module_id) + return LLDB_INVALID_INDEX32; + + return frame_index; +} + size_t ProcessWasm::ReadMemory(const ProcessAddress &process_addr, void *buf, size_t size, Status &error) { + // A caller may reuse one error across reads, as the overridden + // Process::ReadMemory allows. + error.Clear(); + lldb::addr_t vm_addr = process_addr.GetValue(); wasm_addr_t wasm_addr(vm_addr); @@ -184,34 +195,68 @@ ProcessWasm::GetWasmCallStack(lldb::tid_t tid) { } llvm::Expected<lldb::DataBufferSP> -ProcessWasm::GetWasmVariable(WasmVirtualRegisterKinds kind, int frame_index, - int index) { - StreamString packet; - switch (kind) { - case eWasmTagLocal: - packet.Printf("qWasmLocal:"); - break; - case eWasmTagGlobal: - packet.Printf("qWasmGlobal:"); - break; - case eWasmTagOperandStack: - packet.PutCString("qWasmStackValue:"); - break; - case eWasmTagNotAWasmLocation: - return llvm::createStringError("not a Wasm location"); - } - packet.Printf("%d;%d", frame_index, index); - +ProcessWasm::SendWasmValueQuery(llvm::StringRef packet) { StringExtractorGDBRemote response; - if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) != + if (m_gdb_comm.SendPacketAndWaitForResponse(packet, response) != GDBRemoteCommunication::PacketResult::Success) - return llvm::createStringError("failed to send Wasm variable"); + return llvm::createStringErrorV("failed to send {0}", packet); if (!response.IsNormalResponse()) - return llvm::createStringError("failed to get response for Wasm variable"); + return llvm::createStringErrorV("failed to get response for {0}", packet); WritableDataBufferSP buffer_sp( new DataBufferHeap(response.GetStringRef().size() / 2, 0)); response.GetHexBytes(buffer_sp->GetData(), '\xcc'); return buffer_sp; } + +llvm::Expected<lldb::DataBufferSP> +ProcessWasm::GetWasmVariable(WasmVirtualRegisterKinds kind, + uint32_t frame_index, uint32_t index) { + switch (kind) { + case eWasmTagLocal: + return SendWasmValueQuery( + llvm::formatv("qWasmLocal:{0};{1}", frame_index, index).str()); + case eWasmTagOperandStack: + return SendWasmValueQuery( + llvm::formatv("qWasmStackValue:{0};{1}", frame_index, index).str()); + case eWasmTagGlobal: + // A global belongs to a module rather than a frame. See GetWasmGlobal. + return llvm::createStringError("a Wasm global does not belong to a frame"); + case eWasmTagNotAWasmLocation: + return llvm::createStringError("not a Wasm location"); + } + llvm_unreachable("unhandled Wasm virtual register kind"); +} + +llvm::Expected<lldb::DataBufferSP> +ProcessWasm::GetWasmGlobal(uint32_t module_id, uint32_t index, + uint32_t frame_index) { + // The global index space belongs to a module instance, so an index only names + // a global together with the instance holding it. + if (CanNameInstance(module_id)) + return SendWasmValueQuery( + llvm::formatv("qWasmGlobal:{0};instance:{1};", index, module_id).str()); + + // A frame stands in for the instance it is executing only where that instance + // cannot be named. + if (frame_index != LLDB_INVALID_INDEX32) + return SendWasmValueQuery( + llvm::formatv("qWasmGlobal:{0};{1}", frame_index, index).str()); + + if (module_id == kWasmInvalidModuleID) + return llvm::createStringErrorV( + "global {0} belongs to no known module instance, and no frame is " + "executing one to read it through", + index); + + return llvm::createStringErrorV( + "the Wasm stub can only read a global through a frame, and no frame is " + "executing module {0:x} to read global {1} through", + module_id, index); +} + +bool ProcessWasm::CanNameInstance(uint32_t module_id) { + return module_id != kWasmInvalidModuleID && + m_gdb_comm.GetWasmInstanceSupported(); +} diff --git a/lldb/source/Plugins/Process/wasm/ProcessWasm.h b/lldb/source/Plugins/Process/wasm/ProcessWasm.h index 1e73e42af412a..d4a9a9a2be3bd 100644 --- a/lldb/source/Plugins/Process/wasm/ProcessWasm.h +++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.h @@ -46,10 +46,19 @@ class ProcessWasm : public process_gdb_remote::ProcessGDBRemote { /// Retrieve the current call stack from the WebAssembly remote process. llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack(lldb::tid_t tid); - /// Query the value of a WebAssembly variable from the WebAssembly - /// remote process. + /// Query the value of a frame-scoped WebAssembly variable, which is a local + /// or a value on the operand stack. llvm::Expected<lldb::DataBufferSP> - GetWasmVariable(WasmVirtualRegisterKinds kind, int frame_index, int index); + GetWasmVariable(WasmVirtualRegisterKinds kind, uint32_t frame_index, + uint32_t index); + + /// Query the value of a WebAssembly global. The global index space is per + /// module, so an index only names a global together with \a module_id. + /// + /// \a frame_index only serves a stub that cannot be told which instance to + /// read. Pass LLDB_INVALID_INDEX32 when no frame can stand in. + llvm::Expected<lldb::DataBufferSP> + GetWasmGlobal(uint32_t module_id, uint32_t index, uint32_t frame_index); protected: std::shared_ptr<process_gdb_remote::ThreadGDBRemote> @@ -59,12 +68,25 @@ class ProcessWasm : public process_gdb_remote::ProcessGDBRemote { friend class UnwindWasm; friend class ThreadWasm; + /// Ask the WebAssembly stub for a single value, which comes back as the + /// hex-encoded bytes of the whole value. + llvm::Expected<lldb::DataBufferSP> SendWasmValueQuery(llvm::StringRef packet); + /// Read a WebAssembly global by its index in the global index space of the /// module it belongs to. The index space is per module, so an index only /// names a global together with the module it is an index into. size_t ReadGlobal(uint32_t module_id, uint32_t index, void *buf, size_t size, Status &error); + /// The frame to read a global of \a module_id through, or + /// LLDB_INVALID_INDEX32 when no frame can stand in for that module. + uint32_t GetFallbackFrameIndex(uint32_t module_id); + + /// Whether the instance holding a global can be named to the stub, which + /// needs both a valid id to name it by and a stub that accepts one. Where it + /// cannot, a frame executing that instance has to stand in for it. + bool CanNameInstance(uint32_t module_id); + lldb::DynamicRegisterInfoSP &GetRegisterInfo() { return m_register_info_sp; } ProcessWasm(const ProcessWasm &); diff --git a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp index a05fbd56e8680..ee50b3c3bdbbc 100644 --- a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp +++ b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp @@ -29,6 +29,10 @@ RegisterContextWasm::RegisterContextWasm(ThreadGDBRemote &thread, RegisterContextWasm::~RegisterContextWasm() = default; +uint32_t RegisterContextWasm::GetModuleID() { + return GetWasmModuleID(GetPC(LLDB_INVALID_ADDRESS)); +} + uint32_t RegisterContextWasm::ConvertRegisterKindToRegisterNumber( lldb::RegisterKind kind, uint32_t num) { return num; @@ -92,11 +96,15 @@ bool RegisterContextWasm::ReadRegister(const RegisterInfo *reg_info, static_cast<WasmVirtualRegisterInfo *>( const_cast<RegisterInfo *>(reg_info)); - llvm::Expected<DataBufferSP> maybe_buffer = process->GetWasmVariable( - wasm_reg_info->kind, frame_index, wasm_reg_info->index); + llvm::Expected<DataBufferSP> maybe_buffer = + wasm_reg_info->kind == eWasmTagGlobal + ? process->GetWasmGlobal(GetModuleID(), wasm_reg_info->index, + frame_index) + : process->GetWasmVariable(wasm_reg_info->kind, frame_index, + wasm_reg_info->index); if (!maybe_buffer) { LLDB_LOG_ERROR(GetLog(LLDBLog::Process), maybe_buffer.takeError(), - "Failed to read Wasm local: {0}"); + "Failed to read Wasm value: {0}"); return false; } diff --git a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h index 5f047be432a05..ea2cffe811130 100644 --- a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h +++ b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h @@ -60,6 +60,13 @@ class RegisterContextWasm const RegisterValue &value) override; private: + /// The module whose code this context's frame is executing, which a virtual + /// register number has no room to carry. The frame's program counter is where + /// it comes from. Resolved on each use rather than held onto: the innermost + /// frame's context is the thread's own and outlives a stop, so a cached + /// answer would go on naming the module of a previous stop. + uint32_t GetModuleID(); + std::unordered_map<size_t, std::unique_ptr<WasmVirtualRegisterInfo>> m_register_map; }; diff --git a/lldb/source/Plugins/Process/wasm/UnwindWasm.cpp b/lldb/source/Plugins/Process/wasm/UnwindWasm.cpp index df3b146efca14..700ce950fd635 100644 --- a/lldb/source/Plugins/Process/wasm/UnwindWasm.cpp +++ b/lldb/source/Plugins/Process/wasm/UnwindWasm.cpp @@ -26,7 +26,8 @@ static constexpr lldb::addr_t kWasmSyntheticCFABase = 0x40000000; lldb::RegisterContextSP UnwindWasm::DoCreateRegisterContextForFrame(lldb_private::StackFrame *frame) { - if (m_frames.size() <= frame->GetFrameIndex()) + const uint32_t concrete_frame_idx = frame->GetConcreteFrameIndex(); + if (m_frames.size() <= concrete_frame_idx) return lldb::RegisterContextSP(); ThreadSP thread = frame->GetThread(); @@ -34,8 +35,7 @@ UnwindWasm::DoCreateRegisterContextForFrame(lldb_private::StackFrame *frame) { ProcessWasm *wasm_process = static_cast<ProcessWasm *>(thread->GetProcess().get()); - return std::make_shared<RegisterContextWasm>(*gdb_thread, - frame->GetConcreteFrameIndex(), + return std::make_shared<RegisterContextWasm>(*gdb_thread, concrete_frame_idx, wasm_process->GetRegisterInfo()); } diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py b/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py index 4e4a21d9416b1..1d284be6fd7ed 100644 --- a/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py +++ b/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py @@ -6,16 +6,97 @@ from lldbsuite.test.gdbclientutils import * from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase -MODULE_ID = 4 -LOAD_ADDRESS = MODULE_ID << 32 +# Ids of the instances the fake stub loads. An id is written in base 10, both +# in a packet and in the library list it is reported through, so these read +# differently in base 16, which a single digit would not. +MODULE_ID = 16 +SECOND_MODULE_ID = 26 + +# The address spaces an address can point into, in the bits above the id of the +# instance the address belongs to. The object space holds the module image, so +# that is the space a module is loaded in. +WASM_OBJECT_ADDRESS = 1 << 62 +WASM_GLOBAL_ADDRESS = 2 << 62 +WASM_ID_MASK = 0x3FFFFFFF + +LOAD_ADDRESS = WASM_OBJECT_ADDRESS | (MODULE_ID << 32) WASM_LOCAL_ADDR = 0x103E0 -# The synthetic address space globals are given, in which the offset is the -# index into the global index space rather than a byte offset. -WASM_GLOBAL_ADDRESS = 2 << 62 +# The key under which a packet names the module instance whose state it reads. +# The key form is also what tells the two shapes of qWasmGlobal apart, since the +# first field is a global index in one and a frame index in the other. +INSTANCE_KEY = "instance:" + +# Globals the fake stub holds, as index -> (size in bytes, value). Every +# instance has an index space of its own, so these are the globals of one +# instance and the same index names a different global in another. An index is +# written in base 10 like an instance id, so one of them reads differently in +# base 16. +WASM_GLOBALS = {0: (4, 0x2A), 1: (8, 0xDEADBEEF), 26: (4, 0x33)} +SECOND_WASM_GLOBALS = {0: (4, 0x1234), 1: (8, 0xFEEDFACE)} + +# Bytes of the fake stack frame a frame base points at, holding the values of the +# parameters of "add" at the offsets its DWARF gives them, and above them those +# of the variables of "main". A Wasm stack grows down, so the frame of a caller +# sits above the frame of what it called. +WASM_FRAME_BYTES = bytes.fromhex( + "0000000000000000020000000100000000000000020000000100000000000000" +) +WASM_CALLER_FRAME_ADDR = WASM_LOCAL_ADDR + 16 + +# The globals simple_global_frame_base.yaml gives the two functions of the module +# as their frame base. Each function is based on a global of its own, so a read +# of the wrong one does not land in the frame it belongs to. +INNER_FRAME_BASE_GLOBAL_INDEX = 0 +OUTER_FRAME_BASE_GLOBAL_INDEX = 1 +FRAME_BASE_GLOBALS = { + INNER_FRAME_BASE_GLOBAL_INDEX: (4, WASM_LOCAL_ADDR), + OUTER_FRAME_BASE_GLOBAL_INDEX: (4, WASM_CALLER_FRAME_ADDR), +} + + +class WasmModule: + """ + A module the fake stub has loaded, together with the globals held by the + instance it was loaded as. + """ -# Globals the fake engine holds, as index -> (size in bytes, value). -WASM_GLOBALS = {0: (4, 0x2A), 1: (8, 0xDEADBEEF)} + def __init__(self, obj_path, name, module_id=MODULE_ID, global_values=None): + self.obj_path = obj_path + self.name = name + self.module_id = module_id + self.load_address = WASM_OBJECT_ADDRESS | (module_id << 32) + self.global_values = WASM_GLOBALS if global_values is None else global_values + self._image = None + + def get_image(self): + """ + The bytes of the module itself, which is what the stub serves in the + range the module is loaded at. + """ + if self._image is None: + with open(self.obj_path, mode="rb") as file: + self._image = file.read() + return self._image + + def encode_global(self, global_index): + """ + Encode the global at the given index, or an error when this instance + holds no such global. A global is transferred as a whole value, in + little-endian order. + """ + value = self.global_values.get(global_index) + if value is None: + return "E03" + size, val = value + return val.to_bytes(size, "little").hex() + + +def global_read_packet(global_index, module_id=MODULE_ID): + """ + The packet that reads a global from the module instance holding it. + """ + return f"qWasmGlobal:{global_index};{INSTANCE_KEY}{module_id};" def format_register_value(val): @@ -34,17 +115,21 @@ def format_register_value(val): class WasmStackFrame: - def __init__(self, address): + def __init__(self, address, load_address=LOAD_ADDRESS): self._address = address + self._load_address = load_address def __str__(self): - return format_register_value(LOAD_ADDRESS | self._address) + return format_register_value(self._load_address | self._address) class WasmCallStack: def __init__(self, wasm_stack_frames): self._wasm_stack_frames = wasm_stack_frames + def __len__(self): + return len(self._wasm_stack_frames) + def __str__(self): result = "" for frame in self._wasm_stack_frames: @@ -80,38 +165,80 @@ def contains(self, addr): class MyResponder(MockGDBServerResponder): current_pc = LOAD_ADDRESS | 0x01AD - def __init__(self, obj_path, module_name="", wasm_call_stacks=[], memory=None): - self._obj_path = obj_path - self._module_name = module_name or obj_path + def __init__( + self, + modules, + wasm_call_stacks=[], + memory=None, + supports_instance=True, + ): + self._modules = modules self._wasm_call_stacks = wasm_call_stacks self._call_stack_request_count = 0 + self._reported_frames = 0 self._memory = memory + self._supports_instance = supports_instance MockGDBServerResponder.__init__(self) - def respond(self, packet): - if packet[0:13] == "qRegisterInfo": - return self.qRegisterInfo(packet[13:]) + def other(self, packet): if packet.startswith("qWasmCallStack"): return self.qWasmCallStack() if packet.startswith("qWasmLocal"): return self.qWasmLocal(packet) if packet.startswith("qWasmGlobal"): return self.qWasmGlobal(packet) - return MockGDBServerResponder.respond(self, packet) + return MockGDBServerResponder.other(self, packet) + + def module_with_id(self, module_id): + """ + The module the stub loaded with the given id, if it loaded one. Nothing + about a module is shared with another, so an id that names none has no + code to read and no globals to hand out. + """ + for module in self._modules: + if module.module_id == module_id: + return module + return None def qWasmGlobal(self, packet): - # Format: qWasmGlobal:frame_index;index - data = packet.split(":")[1].split(";") - _, global_index = data - value = WASM_GLOBALS.get(int(global_index)) - if value is None: - return "E03" - # A global is transferred as a whole value, in little-endian order. - size, val = value - return val.to_bytes(size, "little").hex() + """ + Read a global. A client that can name the instance holding it does so + with the instance suffix, and one that cannot names a frame. + + Format: qWasmGlobal:index;instance:id; or + qWasmGlobal:frame_index;index + """ + first, _, rest = packet.split(":", 1)[1].partition(";") + + if rest.startswith(INSTANCE_KEY): + if not self._supports_instance: + # A stub is only asked for what it advertised. + return "E05" + # The global index space belongs to the instance, so an index only + # names a global together with the instance it indexes. Another + # instance's globals answer a different question. + module = self.module_with_id(int(rest[len(INSTANCE_KEY) :].rstrip(";"))) + if module is None: + return "E04" + return module.encode_global(int(first)) + + if self._supports_instance: + # A frame cannot name the instance whose globals are indexed, so a + # stub that can be given an instance is never given a frame. + return "E05" + # A frame index that names none of the frames the stub reported is no + # scope to read a global through. + if int(first) >= self._reported_frames: + return "E06" + # A frame stands in for the instance it is executing, which with a single + # loaded module is that module. + return self._modules[0].encode_global(int(rest)) def qSupported(self, client_supported): - return "qXfer:libraries:read+;PacketSize=1000;vContSupported-" + response = "qXfer:libraries:read+;PacketSize=1000;vContSupported-" + if self._supports_instance: + response += ";qWasmInstance+" + return response def qHostInfo(self): return "" @@ -138,11 +265,12 @@ def readRegister(self, register): def qXferRead(self, obj, annex, offset, length): if obj == "libraries": - xml = ( - '<library-list><library name="%s"><section address="%d"/></library></library-list>' - % (self._module_name, LOAD_ADDRESS) + libraries = "".join( + '<library name="%s"><section address="%d"/></library>' + % (module.name, module.load_address) + for module in self._modules ) - return xml, False + return "<library-list>" + libraries + "</library-list>", False else: return None, False @@ -150,19 +278,20 @@ def readMemory(self, addr, length): if self._memory and self._memory.contains(addr): chunk = self._memory.get_bytes(addr, length) return chunk.hex() - if addr < LOAD_ADDRESS: + # A module is loaded in the object space of its instance, so an address + # in that space is what asks for the module image, and the id the address + # carries picks the instance whose module answers. + if addr >> 62 != WASM_OBJECT_ADDRESS >> 62: return "E02" - result = "" - with open(self._obj_path, mode="rb") as file: - file_content = bytearray(file.read()) - if addr >= LOAD_ADDRESS + len(file_content): - return "E03" - addr_from = addr - LOAD_ADDRESS - addr_to = addr_from + min(length, len(file_content) - addr_from) - for i in range(addr_from, addr_to): - result += format(file_content[i], "02x") - file.close() - return result + module = self.module_with_id((addr >> 32) & WASM_ID_MASK) + if module is None: + return "E02" + image = module.get_image() + offset = addr - module.load_address + if offset >= len(image): + return "E03" + end = offset + min(length, len(image) - offset) + return image[offset:end].hex() def setBreakpoint(self, packet): bp_data = packet[1:].split(",") @@ -173,19 +302,20 @@ def qfThreadInfo(self): return "m1" def cont(self): - # Continue execution. Simulates running the Wasm engine until a breakpoint is hit. - return ( - "T05thread-pcs:" - + format(int(self._bp_address, 16) & 0x3FFFFFFFFFFFFFFF, "x") - + ";thread:1" - ) + # Continue execution. Simulates running the Wasm stub until a breakpoint is hit. + # A program counter names the space and the instance it belongs to, as + # the address the breakpoint was set at does. + return "T05thread-pcs:" + self._bp_address + ";thread:1" def qWasmCallStack(self): if len(self._wasm_call_stacks) == 0: return "" - result = str(self._wasm_call_stacks[self._call_stack_request_count]) + call_stack = self._wasm_call_stacks[self._call_stack_request_count] self._call_stack_request_count += 1 - return result + # A frame is only a scope the stub can be asked about once it has + # reported it. + self._reported_frames = len(call_stack) + return str(call_stack) def qWasmLocal(self, packet): # Format: qWasmLocal:frame_index;index @@ -201,14 +331,14 @@ class TestWasm(GDBRemoteTestBase): @skipIfAsan @skipIfXmlSupportMissing def test_load_module_with_embedded_symbols_from_remote(self): - """Test connecting to a WebAssembly engine via GDB-remote and loading a Wasm module with embedded DWARF symbols""" + """Test connecting to a WebAssembly stub via GDB-remote and loading a Wasm module with embedded DWARF symbols""" yaml_path = "test_wasm_embedded_debug_sections.yaml" yaml_base, ext = os.path.splitext(yaml_path) obj_path = self.getBuildArtifact(yaml_base) self.yaml2obj(yaml_path, obj_path) - self.server.responder = MyResponder(obj_path, "test_wasm") + self.server.responder = MyResponder([WasmModule(obj_path, "test_wasm")]) target = self.dbg.CreateTarget("") process = self.connect(target, "wasm") @@ -261,7 +391,7 @@ def test_load_module_with_embedded_symbols_from_remote(self): @skipIfAsan @skipIfXmlSupportMissing def test_load_module_with_stripped_symbols_from_remote(self): - """Test connecting to a WebAssembly engine via GDB-remote and loading a Wasm module with symbols stripped into a separate Wasm file""" + """Test connecting to a WebAssembly stub via GDB-remote and loading a Wasm module with symbols stripped into a separate Wasm file""" sym_yaml_path = "test_sym.yaml" sym_yaml_base, ext = os.path.splitext(sym_yaml_path) @@ -273,7 +403,7 @@ def test_load_module_with_stripped_symbols_from_remote(self): obj_path = self.getBuildArtifact(yaml_base) + ".wasm" self.yaml2obj(yaml_path, obj_path) - self.server.responder = MyResponder(obj_path, "test_wasm") + self.server.responder = MyResponder([WasmModule(obj_path, "test_wasm")]) folder, _ = os.path.split(obj_path) self.runCmd( @@ -327,7 +457,7 @@ def test_load_module_with_stripped_symbols_from_remote(self): @skipIfAsan @skipIfXmlSupportMissing def test_simple_wasm_debugging_session(self): - """Test connecting to a WebAssembly engine via GDB-remote, loading a + """Test connecting to a WebAssembly stub via GDB-remote, loading a Wasm module with embedded DWARF symbols, setting a breakpoint and checking the debuggee state""" @@ -350,15 +480,10 @@ def test_simple_wasm_debugging_session(self): # Create fake memory for our wasm locals. self.memory = FakeMemory(0x10000, 0x20000) - self.memory.store_bytes( - WASM_LOCAL_ADDR, - bytes.fromhex( - "0000000000000000020000000100000000000000020000000100000000000000" - ), - ) + self.memory.store_bytes(WASM_LOCAL_ADDR, WASM_FRAME_BYTES) self.server.responder = MyResponder( - obj_path, "test_wasm", call_stacks, self.memory + [WasmModule(obj_path, "test_wasm")], call_stacks, self.memory ) target = self.dbg.CreateTarget("") @@ -422,7 +547,9 @@ def test_max_backtrace_depth(self): [WasmStackFrame(0x019C), WasmStackFrame(0x01E5), WasmStackFrame(0x01FE)] ), ] - self.server.responder = MyResponder(obj_path, "test_wasm", call_stacks) + self.server.responder = MyResponder( + [WasmModule(obj_path, "test_wasm")], call_stacks + ) target = self.dbg.CreateTarget("") process = self.connect(target, "wasm") @@ -445,20 +572,29 @@ def test_max_backtrace_depth(self): self.assertEqual(thread.GetFrameAtIndex(1).GetPC(), LOAD_ADDRESS | 0x01E5) self.assertFalse(thread.GetFrameAtIndex(2).IsValid()) - @skipIfAsan - @skipIfXmlSupportMissing - def test_read_global(self): - """Test that a WebAssembly global can be read through the address its - module gives it, and that a read it cannot serve fails instead of - returning something plausible.""" - - yaml_path = "simple.yaml" - yaml_base, _ = os.path.splitext(yaml_path) - obj_path = self.getBuildArtifact(yaml_base) + def build_wasm_module(self, name, yaml_path="simple.yaml", **module_args): + """ + Build a Wasm module the fake stub reports under the given name. A + loaded module is told apart from another by the name it is reported + under, so every instance has to be reported under a name of its own. + """ + obj_path = self.getBuildArtifact(name) self.yaml2obj(yaml_path, obj_path) - - call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] - self.server.responder = MyResponder(obj_path, "test_wasm", call_stacks) + return WasmModule(obj_path, name, **module_args) + + def connect_to_modules( + self, modules, call_stacks, supports_instance=True, memory=None + ): + """ + Connect to a fake stub holding the given modules and return its target + and process. + """ + self.server.responder = MyResponder( + modules, + call_stacks, + memory, + supports_instance=supports_instance, + ) target = self.dbg.CreateTarget("") process = self.connect(target, "wasm") @@ -466,14 +602,93 @@ def test_read_global(self): self, self.dbg.GetListener(), process, [lldb.eStateStopped] ) - # Read through the address the module gives its globals rather than a - # constructed one, so that the encoding the object file produces and the - # one the process decodes are checked against each other. - module = target.GetModuleAtIndex(0) - global_section = module.FindSection("global") + return target, process + + def get_globals_address(self, target, module): + """ + The address the loaded module gives its globals. Reading through it + rather than a constructed address checks the encoding the object file + produces and the one the process decodes against each other. + """ + image = target.FindModule(lldb.SBFileSpec(module.name)) + self.assertTrue(image.IsValid()) + global_section = image.FindSection("global") self.assertTrue(global_section.IsValid()) globals_addr = global_section.GetLoadAddress(target) - self.assertEqual(globals_addr, WASM_GLOBAL_ADDRESS | LOAD_ADDRESS) + # A global lives in the global index space of its instance rather than + # in the space the module itself is loaded in. + self.assertEqual(globals_addr, WASM_GLOBAL_ADDRESS | (module.module_id << 32)) + return globals_addr + + def connect_to_globals(self, call_stacks, supports_instance=True): + """ + Connect to a fake stub holding WASM_GLOBALS and return its process + together with the address its module gives its globals. + """ + module = self.build_wasm_module("test_wasm") + target, process = self.connect_to_modules( + [module], call_stacks, supports_instance + ) + return process, self.get_globals_address(target, module) + + def connect_to_frame_base_globals(self, supports_instance=True, call_stacks=None): + """ + Connect to a fake stub stopped in a module whose functions are based on + a global holding the address of their frame, and return its thread. + """ + module = self.build_wasm_module( + "test_wasm", + yaml_path="simple_global_frame_base.yaml", + global_values=FRAME_BASE_GLOBALS, + ) + + if call_stacks is None: + call_stacks = [ + WasmCallStack([WasmStackFrame(0x019C), WasmStackFrame(0x01E5)]) + ] + memory = FakeMemory(0x10000, 0x20000) + memory.store_bytes(WASM_LOCAL_ADDR, WASM_FRAME_BYTES) + _, process = self.connect_to_modules( + [module], + call_stacks, + supports_instance=supports_instance, + memory=memory, + ) + + thread = process.GetThreadAtIndex(0) + self.assertTrue(thread.IsValid()) + return thread + + def packets_received(self, prefix): + """ + The packets the client sent that start with the given prefix. An + assertion on the packets themselves names the ones that make it fail, + which an assertion on whether there are any does not. + """ + received = self.server.responder.packetLog.get_received() + return [packet for packet in received if packet.startswith(prefix)] + + def global_reads_received(self, instance): + """ + The global reads the client sent that name an instance, or those that + name a frame instead. Both forms share a packet name, so the suffix is + what tells them apart. + """ + return [ + packet + for packet in self.packets_received("qWasmGlobal:") + if (INSTANCE_KEY in packet) == instance + ] + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global(self): + """Test that a WebAssembly global can be read through the address its + module gives it, and that a read it cannot serve fails instead of + returning something plausible.""" + + call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] + process, globals_addr = self.connect_to_globals(call_stacks) # A global is read as a whole value. error = lldb.SBError() @@ -485,6 +700,10 @@ def test_read_global(self): self.assertSuccess(error) self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) + data = process.ReadMemory(globals_addr + 26, 4, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0x33) + # A type narrower than the global it is held in reads the low bytes, # which is how a char or short global is read. data = process.ReadMemory(globals_addr + 0, 1, error) @@ -502,6 +721,314 @@ def test_read_global(self): process.ReadMemory(globals_addr + 99, 4, error) self.assertFalse(error.Success()) + # A global is named by the instance holding it. MODULE_ID differs from + # every frame index, so a frame index cannot pass for an instance id. + self.assertPacketLogReceived( + [ + global_read_packet(0), + global_read_packet(1), + global_read_packet(26), + ] + ) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_without_reported_call_stack(self): + """Test that a WebAssembly global can be read while the stub reports no + call stack for the instance holding it.""" + + process, globals_addr = self.connect_to_globals([]) + + # A thread always has a frame, which LLDB makes from the registers the + # stub reports when the Wasm unwinder contributes none. The program + # counter of that frame belongs to no module, so it is not a frame a + # global can be read through. + thread = process.GetThreadAtIndex(0) + self.assertTrue(thread.IsValid()) + self.assertEqual(1, thread.GetNumFrames()) + self.assertEqual(MyResponder.current_pc, thread.GetFrameAtIndex(0).GetPC()) + + error = lldb.SBError() + data = process.ReadMemory(globals_addr + 0, 4, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0x2A) + + data = process.ReadMemory(globals_addr + 1, 8, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) + + self.assertPacketLogReceived( + [ + global_read_packet(0), + global_read_packet(1), + ] + ) + + # The stub was asked for a call stack and reported none, so the reads + # above were served without one. Reading a global through a frame is + # something else, and is what a stub that cannot be told which instance + # to read is limited to. + self.assertNotEqual([], self.packets_received("qWasmCallStack")) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_from_multiple_instances(self): + """Test that a global is read from the instance that holds it when more + than one instance is loaded.""" + + module = self.build_wasm_module("test_wasm") + second_module = self.build_wasm_module( + "test_wasm_second", + module_id=SECOND_MODULE_ID, + global_values=SECOND_WASM_GLOBALS, + ) + + # Only the first instance is executing, so the frame available to read a + # global through belongs to it and cannot stand in for the other one. + call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] + target, process = self.connect_to_modules([module, second_module], call_stacks) + + # Each instance has to be a module of its own for its globals to be its + # own. Collapsing the two would leave one of the load addresses unused. + self.assertEqual(2, target.GetNumModules()) + + globals_addr = self.get_globals_address(target, module) + second_globals_addr = self.get_globals_address(target, second_module) + self.assertNotEqual(globals_addr, second_globals_addr) + + # The same index in either instance names a global of that instance, and + # the fake stub refuses any instance it did not load, so neither value + # can come from the wrong place. + error = lldb.SBError() + for addr, global_values in [ + (globals_addr, WASM_GLOBALS), + (second_globals_addr, SECOND_WASM_GLOBALS), + ]: + for index, (size, value) in global_values.items(): + data = process.ReadMemory(addr + index, size, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), value) + + self.assertPacketLogReceived( + [ + global_read_packet(0), + global_read_packet(1), + global_read_packet(0, SECOND_MODULE_ID), + global_read_packet(1, SECOND_MODULE_ID), + ] + ) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_from_default_instance(self): + """Test that a global is read from the instance a debug session is + created for, which a stub reports at id zero.""" + + # An id of zero yields the load address a stub reports when it cannot + # report an instance at all, and an address the running code computed + # carries no id either, so zero has to name an instance. + module = self.build_wasm_module("test_wasm", module_id=0) + self.assertEqual(module.load_address, WASM_OBJECT_ADDRESS) + + call_stacks = [WasmCallStack([WasmStackFrame(0x019C, module.load_address)])] + target, process = self.connect_to_modules([module], call_stacks) + globals_addr = self.get_globals_address(target, module) + + error = lldb.SBError() + data = process.ReadMemory(globals_addr + 0, 4, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0x2A) + + # Zero names that instance rather than standing for no instance, so the + # read is scoped to it instead of falling back to naming a frame. + self.assertPacketLogReceived([global_read_packet(0, module.module_id)]) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_variable_located_through_global(self): + """Test that a variable an outer frame locates through a Wasm global is + read from the instance the frame is executing.""" + + thread = self.connect_to_frame_base_globals() + frame1 = thread.GetFrameAtIndex(1) + self.assertTrue(frame1.IsValid()) + self.assertIn("main", frame1.GetFunctionName()) + + # A variable relative to that frame base is only reachable by reading the + # global, so these values are what came back from it. + i = frame1.FindVariable("i") + self.assertTrue(i.IsValid()) + self.assertEqual(i.GetValueAsUnsigned(), 1) + + j = frame1.FindVariable("j") + self.assertTrue(j.IsValid()) + self.assertEqual(j.GetValueAsUnsigned(), 2) + + # The global belongs to the instance the frame is executing, and that + # instance is what names it. The frame the read goes through does not. + self.assertPacketLogReceived( + [global_read_packet(OUTER_FRAME_BASE_GLOBAL_INDEX)] + ) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_variable_located_through_global_in_innermost_frame(self): + """Test that a variable the innermost frame locates through a Wasm global + is read from the instance that frame is executing.""" + + # The innermost frame is where a stop leaves the program, and its + # register context is the one the thread carries, which exists before any + # frame does. The instance that frame is executing is therefore only + # known once the stub has been asked for its call stack. + thread = self.connect_to_frame_base_globals() + frame0 = thread.GetFrameAtIndex(0) + self.assertTrue(frame0.IsValid()) + self.assertIn("add", frame0.GetFunctionName()) + + a = frame0.FindVariable("a") + self.assertTrue(a.IsValid()) + self.assertEqual(a.GetValueAsUnsigned(), 1) + + b = frame0.FindVariable("b") + self.assertTrue(b.IsValid()) + self.assertEqual(b.GetValueAsUnsigned(), 2) + + self.assertPacketLogReceived( + [global_read_packet(INNER_FRAME_BASE_GLOBAL_INDEX)] + ) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_variable_located_through_global_without_reported_call_stack(self): + """Test that a variable a frame locates through a Wasm global is read + from the instance that frame is executing while the stub reports no call + stack.""" + + # LLDB makes a frame from the registers the stub reports when the Wasm + # unwinder contributes none. The program counter of that frame is the + # only place the instance it executes is recorded, so a read that looked + # for the instance anywhere else would be left without one and fall back + # to naming a frame the stub never reported. + thread = self.connect_to_frame_base_globals(call_stacks=[]) + self.assertEqual(1, thread.GetNumFrames()) + + frame0 = thread.GetFrameAtIndex(0) + self.assertTrue(frame0.IsValid()) + self.assertIn("add", frame0.GetFunctionName()) + + a = frame0.FindVariable("a") + self.assertTrue(a.IsValid()) + self.assertEqual(a.GetValueAsUnsigned(), 1) + + self.assertPacketLogReceived( + [global_read_packet(INNER_FRAME_BASE_GLOBAL_INDEX)] + ) + self.assertEqual([], self.global_reads_received(instance=False)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_variable_located_through_global_legacy_server(self): + """Test that a variable the innermost frame locates through a Wasm global + is read through that frame when the stub cannot be told which instance to + read.""" + + thread = self.connect_to_frame_base_globals(supports_instance=False) + frame0 = thread.GetFrameAtIndex(0) + self.assertTrue(frame0.IsValid()) + self.assertIn("add", frame0.GetFunctionName()) + + a = frame0.FindVariable("a") + self.assertTrue(a.IsValid()) + self.assertEqual(a.GetValueAsUnsigned(), 1) + + # A frame stands in for the instance it is executing, which is all such a + # stub can be asked, so the same global stays within reach through the + # frame. + self.assertPacketLogReceived(["qWasmGlobal:0;0"]) + self.assertEqual([], self.global_reads_received(instance=True)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_legacy_server(self): + """Test that a global is read through the frame executing the instance + that holds it when the stub cannot be told which instance to read.""" + + call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] + process, globals_addr = self.connect_to_globals( + call_stacks, supports_instance=False + ) + + error = lldb.SBError() + data = process.ReadMemory(globals_addr + 0, 4, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0x2A) + + # A global at an index the frame index cannot be mistaken for, so that + # the frame and the global keep their places in the packet. + data = process.ReadMemory(globals_addr + 1, 8, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) + + # A stub is only asked for what it advertised, so the form it never + # offered is not tried on it. + self.assertPacketLogReceived(["qWasmGlobal:0;0", "qWasmGlobal:0;1"]) + self.assertEqual([], self.global_reads_received(instance=True)) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_legacy_server_without_reported_call_stack(self): + """Test that a global is out of reach of a stub that can only read a + global through a frame while it reports no call stack.""" + + process, globals_addr = self.connect_to_globals([], supports_instance=False) + + error = lldb.SBError() + process.ReadMemory(globals_addr + 0, 4, error) + self.assertFalse(error.Success()) + self.assertIn("can only read a global through a frame", error.GetCString()) + self.assertEqual([], self.packets_received("qWasmGlobal:")) + + @skipIfAsan + @skipIfXmlSupportMissing + def test_read_global_legacy_server_other_instance(self): + """Test that a frame executing one instance is not read as a frame of + another when the stub can only read a global through a frame.""" + + module = self.build_wasm_module("test_wasm") + second_module = self.build_wasm_module( + "test_wasm_second", + module_id=SECOND_MODULE_ID, + global_values=SECOND_WASM_GLOBALS, + ) + + call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] + target, process = self.connect_to_modules( + [module, second_module], call_stacks, supports_instance=False + ) + self.assertEqual(2, target.GetNumModules()) + + # The only frame is executing the first instance, and a frame of one + # instance indexes no globals of another. Reading a global of the second + # instance fails rather than answering with a global of the first. + error = lldb.SBError() + process.ReadMemory(self.get_globals_address(target, second_module), 4, error) + self.assertFalse(error.Success()) + self.assertIn("can only read a global through a frame", error.GetCString()) + + # The instance the frame is executing is still within reach. + error = lldb.SBError() + data = process.ReadMemory(self.get_globals_address(target, module), 4, error) + self.assertSuccess(error) + self.assertEqual(int.from_bytes(data, "little"), WASM_GLOBALS[0][1]) + + self.assertPacketLogReceived(["qWasmGlobal:0;0"]) + @skipIfXmlSupportMissing def test_non_wasm_process(self): """Test that the plugin falls back to plain GDB remote debugging when diff --git a/lldb/test/API/functionalities/gdb_remote_client/simple_global_frame_base.yaml b/lldb/test/API/functionalities/gdb_remote_client/simple_global_frame_base.yaml new file mode 100644 index 0000000000000..6dff712d9f539 --- /dev/null +++ b/lldb/test/API/functionalities/gdb_remote_client/simple_global_frame_base.yaml @@ -0,0 +1,238 @@ +# simple.yaml, with the frame base of both of its functions changed from a Wasm +# local to a Wasm global: +# +# "add": DW_AT_frame_base (DW_OP_WASM_location 0x1 0x0, DW_OP_stack_value) +# "main": DW_AT_frame_base (DW_OP_WASM_location 0x1 0x1, DW_OP_stack_value) +# +# A DW_OP_WASM_location names a local, a global or an operand stack value, so +# a frame based on a global is what makes resolving a variable in that frame +# read a global. Each function is based on a global of its own, so a read that +# went to the wrong one would not land in the frame it belongs to. +--- !WASM +FileHeader: + Version: 0x1 +Sections: + - Type: TYPE + Signatures: + - Index: 0 + ParamTypes: [] + ReturnTypes: [] + - Index: 1 + ParamTypes: + - I32 + - I32 + ReturnTypes: + - I32 + - Index: 2 + ParamTypes: [] + ReturnTypes: + - I32 + - Type: FUNCTION + FunctionTypes: [ 0, 1, 2, 1 ] + - Type: TABLE + Tables: + - Index: 0 + ElemType: FUNCREF + Limits: + Flags: [ HAS_MAX ] + Minimum: 0x1 + Maximum: 0x1 + - Type: MEMORY + Memories: + - Minimum: 0x2 + - Type: GLOBAL + Globals: + - Index: 0 + Type: I32 + Mutable: true + InitExpr: + Opcode: I32_CONST + Value: 66560 + - Index: 1 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 1024 + - Index: 2 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 1024 + - Index: 3 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 1024 + - Index: 4 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 66560 + - Index: 5 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 1024 + - Index: 6 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 66560 + - Index: 7 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 131072 + - Index: 8 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 0 + - Index: 9 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 1 + - Index: 10 + Type: I32 + Mutable: false + InitExpr: + Opcode: I32_CONST + Value: 65536 + - Type: EXPORT + Exports: + - Name: memory + Kind: MEMORY + Index: 0 + - Name: __wasm_call_ctors + Kind: FUNCTION + Index: 0 + - Name: add + Kind: FUNCTION + Index: 1 + - Name: __original_main + Kind: FUNCTION + Index: 2 + - Name: main + Kind: FUNCTION + Index: 3 + - Name: __main_void + Kind: FUNCTION + Index: 2 + - Name: __indirect_function_table + Kind: TABLE + Index: 0 + - Name: __dso_handle + Kind: GLOBAL + Index: 1 + - Name: __data_end + Kind: GLOBAL + Index: 2 + - Name: __stack_low + Kind: GLOBAL + Index: 3 + - Name: __stack_high + Kind: GLOBAL + Index: 4 + - Name: __global_base + Kind: GLOBAL + Index: 5 + - Name: __heap_base + Kind: GLOBAL + Index: 6 + - Name: __heap_end + Kind: GLOBAL + Index: 7 + - Name: __memory_base + Kind: GLOBAL + Index: 8 + - Name: __table_base + Kind: GLOBAL + Index: 9 + - Name: __wasm_first_page_end + Kind: GLOBAL + Index: 10 + - Type: CODE + Functions: + - Index: 0 + Locals: [] + Body: 0B + - Index: 1 + Locals: + - Type: I32 + Count: 1 + Body: 23808080800041106B21022002200036020C20022001360208200228020C20022802086A0F0B + - Index: 2 + Locals: + - Type: I32 + Count: 2 + Body: 23808080800041106B210020002480808080002000410036020C2000410136020820004102360204200028020820002802041081808080002101200041106A24808080800020010F0B + - Index: 3 + Locals: [] + Body: 1082808080000F0B + - Type: CUSTOM + Name: .debug_abbrev + Payload: 011101250E1305030E10171B0E110155170000022E01110112064018030E3A0B3B0B271949133F1900000305000218030E3A0B3B0B49130000042E01110112064018030E3A0B3B0B49133F1900000534000218030E3A0B3B0B49130000062400030E3E0B0B0B000000 + - Type: CUSTOM + Name: .debug_info + Payload: 940000000400000000000401620000001D0055000000000000000D000000000000000000000002050000002900000004ED01009F510000000101900000000302910C60000000010190000000030291085E00000001019000000000042F0000004C00000004ED01019F04000000010690000000050291080B0000000107900000000502910409000000010890000000000600000000050400 + - Type: CUSTOM + Name: .debug_ranges + Payload: 050000002E0000002F0000007B0000000000000000000000 + - Type: CUSTOM + Name: .debug_str + Payload: 696E74006D61696E006A0069002F55736572732F6A6F6E61732F7761736D2D6D6963726F2D72756E74696D652F70726F647563742D6D696E692F706C6174666F726D732F64617277696E2F6275696C64006164640073696D706C652E630062006100636C616E672076657273696F6E2032322E302E306769742028676974406769746875622E636F6D3A4A4465766C696567686572652F6C6C766D2D70726F6A6563742E67697420343161363839613132323834633834623632383933393461356338306264636534383733656466302900 + - Type: CUSTOM + Name: .debug_line + Payload: 62000000040020000000010101FB0E0D0001010101000000010000010073696D706C652E6300000000000005020500000001050A0A08AE050E0658050C5805032002020001010005022F0000001705070A08BB75050E7505110658050A58050382020F000101 + - Type: CUSTOM + Name: name + FunctionNames: + - Index: 0 + Name: __wasm_call_ctors + - Index: 1 + Name: add + - Index: 2 + Name: __original_main + - Index: 3 + Name: main + GlobalNames: + - Index: 0 + Name: __stack_pointer + - Type: CUSTOM + Name: producers + Languages: + - Name: C11 + Version: '' + Tools: + - Name: clang + Version: '22.0.0git' + - Type: CUSTOM + Name: target_features + Features: + - Prefix: USED + Name: bulk-memory + - Prefix: USED + Name: bulk-memory-opt + - Prefix: USED + Name: call-indirect-overlong + - Prefix: USED + Name: multivalue + - Prefix: USED + Name: mutable-globals + - Prefix: USED + Name: nontrapping-fptoint + - Prefix: USED + Name: reference-types + - Prefix: USED + Name: sign-ext +... >From e3d5c58f2ba4bff151048b0613c0e8843f6c9798 Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere <[email protected]> Date: Wed, 19 Aug 2026 16:36:46 -0700 Subject: [PATCH 2/2] Address Felipe's feedback --- lldb/docs/resources/lldbgdbremote.md | 16 +-- .../Plugins/Process/wasm/ProcessWasm.cpp | 98 ++++++++++--------- .../source/Plugins/Process/wasm/ProcessWasm.h | 42 ++++---- .../Process/wasm/RegisterContextWasm.cpp | 21 +++- .../Process/wasm/RegisterContextWasm.h | 9 +- 5 files changed, 102 insertions(+), 84 deletions(-) diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md index bc39292427c45..5a3f83801ac40 100644 --- a/lldb/docs/resources/lldbgdbremote.md +++ b/lldb/docs/resources/lldbgdbremote.md @@ -2676,26 +2676,26 @@ are encoded as base 10. The result is a hex-encoded little-endian value of the whole global, or `E<nn>`. A global index space belongs to a module instance, so an index only names a -global together with the instance to read it from. A stub that advertises -`qWasmInstance+` is named that instance directly: +global together with the module instance to read it from. A stub that advertises +`qWasmInstance+` requires that module instance to be named explicitly: ``` send packet: $qWasmGlobal:2;instance:16;#32 read packet: $e0030100#b9 ``` -A stub that does not is given a frame index instead, which only reaches the -instance that frame is executing: +A stub that does not advertise `qWasmInstance+` is given a frame index instead, +which only reaches the module instance that frame is executing: ``` send packet: $qWasmGlobal:0;2#31 read packet: $e0030100#b9 ``` -The first field is a global index in the first form and a frame index in the -second, so a stub tells the two apart by the `<key>:<value>` suffix. An -unrecognized instance id must be answered with an error rather than with another -instance's global. +A stub tells the two apart by the presence of the `instance:` key, the only key +this packet takes. Where it is absent, the first field is a frame index rather +than a global index. An unrecognized instance id must be answered with an error +rather than with another instance's global. **Priority to Implement:** Only required for Wasm support. Necessary to show variables. diff --git a/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp index 2c627548c8aaf..3e2bf05e00a0c 100644 --- a/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp +++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp @@ -89,24 +89,16 @@ std::shared_ptr<ThreadGDBRemote> ProcessWasm::CreateThread(lldb::tid_t tid) { return std::make_shared<ThreadWasm>(*this, tid); } -size_t ProcessWasm::ReadGlobal(uint32_t module_id, uint32_t index, void *buf, - size_t size, Status &error) { - // Looking for a frame drives the unwinder, so only pay for it when the read - // has to go through one. - const uint32_t frame_index = CanNameInstance(module_id) - ? LLDB_INVALID_INDEX32 - : GetFallbackFrameIndex(module_id); - - llvm::Expected<lldb::DataBufferSP> buffer = - GetWasmGlobal(module_id, index, frame_index); - if (!buffer) { - error = Status::FromError(buffer.takeError()); +static size_t CopyGlobal(llvm::Expected<lldb::DataBufferSP> global, void *buf, + size_t size, Status &error) { + if (!global) { + error = Status::FromError(global.takeError()); return 0; } // A global comes back whole. Reading more than it holds would have to come // from somewhere else, and the next index is not adjacent storage. - const size_t global_size = (*buffer)->GetByteSize(); + const size_t global_size = (*global)->GetByteSize(); if (size > global_size) { error = Status::FromErrorStringWithFormatv( "Wasm global read failed: requested {0} bytes from a {1}-byte global", @@ -114,24 +106,50 @@ size_t ProcessWasm::ReadGlobal(uint32_t module_id, uint32_t index, void *buf, return 0; } - std::memcpy(buf, (*buffer)->GetBytes(), size); + std::memcpy(buf, (*global)->GetBytes(), size); return size; } -uint32_t ProcessWasm::GetFallbackFrameIndex(uint32_t module_id) { +size_t ProcessWasm::ReadGlobal(uint32_t module_id, uint32_t index, void *buf, + size_t size, Status &error) { + if (CanNameInstance(module_id)) + return CopyGlobal(GetWasmGlobalForModule(module_id, index), buf, size, + error); + + // Looking for a frame drives the unwinder, so only pay for it where the + // instance cannot be named. + llvm::Expected<uint32_t> frame_index = GetFallbackFrameIndex(module_id); + if (!frame_index) { + error = Status::FromError(frame_index.takeError()); + return 0; + } + + return CopyGlobal(GetWasmGlobalForFrame(*frame_index, index), buf, size, + error); +} + +llvm::Expected<uint32_t> +ProcessWasm::GetFallbackFrameIndex(uint32_t module_id) { + if (module_id == kWasmInvalidModuleID) + return llvm::createStringError( + "the global belongs to no known module instance"); + ThreadSP thread = GetThreadList().GetSelectedThread(); StackFrameSP frame = thread ? thread->GetSelectedFrame(DoNoSelectMostRelevantFrame) : nullptr; - if (!frame) - return LLDB_INVALID_INDEX32; - - // A frame can only stand in for the module the stub reports it executing. - const uint32_t frame_index = frame->GetConcreteFrameIndex(); - ThreadWasm &wasm_thread = static_cast<ThreadWasm &>(*thread); - if (GetWasmModuleID(wasm_thread.GetConcreteFramePC(frame_index)) != module_id) - return LLDB_INVALID_INDEX32; + if (frame) { + // A frame can only stand in for the module the stub reports it executing. + const uint32_t frame_index = frame->GetConcreteFrameIndex(); + ThreadWasm &wasm_thread = static_cast<ThreadWasm &>(*thread); + if (GetWasmModuleID(wasm_thread.GetConcreteFramePC(frame_index)) == + module_id) + return frame_index; + } - return frame_index; + return llvm::createStringErrorV( + "the Wasm stub can only read a global through a frame, and no frame is " + "executing module {0:x}", + module_id); } size_t ProcessWasm::ReadMemory(const ProcessAddress &process_addr, void *buf, @@ -221,7 +239,6 @@ ProcessWasm::GetWasmVariable(WasmVirtualRegisterKinds kind, return SendWasmValueQuery( llvm::formatv("qWasmStackValue:{0};{1}", frame_index, index).str()); case eWasmTagGlobal: - // A global belongs to a module rather than a frame. See GetWasmGlobal. return llvm::createStringError("a Wasm global does not belong to a frame"); case eWasmTagNotAWasmLocation: return llvm::createStringError("not a Wasm location"); @@ -230,30 +247,15 @@ ProcessWasm::GetWasmVariable(WasmVirtualRegisterKinds kind, } llvm::Expected<lldb::DataBufferSP> -ProcessWasm::GetWasmGlobal(uint32_t module_id, uint32_t index, - uint32_t frame_index) { - // The global index space belongs to a module instance, so an index only names - // a global together with the instance holding it. - if (CanNameInstance(module_id)) - return SendWasmValueQuery( - llvm::formatv("qWasmGlobal:{0};instance:{1};", index, module_id).str()); - - // A frame stands in for the instance it is executing only where that instance - // cannot be named. - if (frame_index != LLDB_INVALID_INDEX32) - return SendWasmValueQuery( - llvm::formatv("qWasmGlobal:{0};{1}", frame_index, index).str()); - - if (module_id == kWasmInvalidModuleID) - return llvm::createStringErrorV( - "global {0} belongs to no known module instance, and no frame is " - "executing one to read it through", - index); +ProcessWasm::GetWasmGlobalForModule(uint32_t module_id, uint32_t index) { + return SendWasmValueQuery( + llvm::formatv("qWasmGlobal:{0};instance:{1};", index, module_id).str()); +} - return llvm::createStringErrorV( - "the Wasm stub can only read a global through a frame, and no frame is " - "executing module {0:x} to read global {1} through", - module_id, index); +llvm::Expected<lldb::DataBufferSP> +ProcessWasm::GetWasmGlobalForFrame(uint32_t frame_index, uint32_t index) { + return SendWasmValueQuery( + llvm::formatv("qWasmGlobal:{0};{1}", frame_index, index).str()); } bool ProcessWasm::CanNameInstance(uint32_t module_id) { diff --git a/lldb/source/Plugins/Process/wasm/ProcessWasm.h b/lldb/source/Plugins/Process/wasm/ProcessWasm.h index d4a9a9a2be3bd..cffff2fa54093 100644 --- a/lldb/source/Plugins/Process/wasm/ProcessWasm.h +++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.h @@ -52,13 +52,24 @@ class ProcessWasm : public process_gdb_remote::ProcessGDBRemote { GetWasmVariable(WasmVirtualRegisterKinds kind, uint32_t frame_index, uint32_t index); - /// Query the value of a WebAssembly global. The global index space is per - /// module, so an index only names a global together with \a module_id. - /// - /// \a frame_index only serves a stub that cannot be told which instance to - /// read. Pass LLDB_INVALID_INDEX32 when no frame can stand in. - llvm::Expected<lldb::DataBufferSP> - GetWasmGlobal(uint32_t module_id, uint32_t index, uint32_t frame_index); + /// Query the value of the global at \a index in the global index space of the + /// module instance \a module_id names. An index only names a global together + /// with an instance. Only a stub that advertises "qWasmInstance+" can be told + /// which instance to read, so see CanNameInstance first. + llvm::Expected<lldb::DataBufferSP> GetWasmGlobalForModule(uint32_t module_id, + uint32_t index); + + /// Query the value of a WebAssembly global through the frame at \a + /// frame_index, which reaches only the globals of the module instance that + /// frame is executing. This is the only scope a stub that cannot be told + /// which instance to read offers. + llvm::Expected<lldb::DataBufferSP> GetWasmGlobalForFrame(uint32_t frame_index, + uint32_t index); + + /// Whether the instance holding a global can be named to the stub, which + /// needs both a valid id to name it by and a stub that accepts one. Where it + /// cannot, a frame executing that instance has to stand in for it. + bool CanNameInstance(uint32_t module_id); protected: std::shared_ptr<process_gdb_remote::ThreadGDBRemote> @@ -72,20 +83,15 @@ class ProcessWasm : public process_gdb_remote::ProcessGDBRemote { /// hex-encoded bytes of the whole value. llvm::Expected<lldb::DataBufferSP> SendWasmValueQuery(llvm::StringRef packet); - /// Read a WebAssembly global by its index in the global index space of the - /// module it belongs to. The index space is per module, so an index only - /// names a global together with the module it is an index into. + /// Read a WebAssembly global of the module instance \a module_id names, + /// through whichever scope that stub can be asked for it in. size_t ReadGlobal(uint32_t module_id, uint32_t index, void *buf, size_t size, Status &error); - /// The frame to read a global of \a module_id through, or - /// LLDB_INVALID_INDEX32 when no frame can stand in for that module. - uint32_t GetFallbackFrameIndex(uint32_t module_id); - - /// Whether the instance holding a global can be named to the stub, which - /// needs both a valid id to name it by and a stub that accepts one. Where it - /// cannot, a frame executing that instance has to stand in for it. - bool CanNameInstance(uint32_t module_id); + /// The frame to read a global of \a module_id through, for a stub that cannot + /// be told which instance to read. Fails when no frame is executing that + /// module, which leaves its globals out of reach. + llvm::Expected<uint32_t> GetFallbackFrameIndex(uint32_t module_id); lldb::DynamicRegisterInfoSP &GetRegisterInfo() { return m_register_info_sp; } diff --git a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp index ee50b3c3bdbbc..ba3768a3d5ebc 100644 --- a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp +++ b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp @@ -66,6 +66,21 @@ const RegisterSet *RegisterContextWasm::GetRegisterSet(size_t reg_set) { return nullptr; } +/// A local and an operand stack value belong to the frame, while a global +/// belongs to the module instance the frame is executing, which is named +/// directly wherever the stub accepts one. +static llvm::Expected<DataBufferSP> +ReadWasmValue(ProcessWasm &process, const WasmVirtualRegisterInfo ®_info, + uint32_t frame_index, uint32_t module_id) { + if (reg_info.kind != eWasmTagGlobal) + return process.GetWasmVariable(reg_info.kind, frame_index, reg_info.index); + + if (process.CanNameInstance(module_id)) + return process.GetWasmGlobalForModule(module_id, reg_info.index); + + return process.GetWasmGlobalForFrame(frame_index, reg_info.index); +} + bool RegisterContextWasm::ReadRegister(const RegisterInfo *reg_info, RegisterValue &value) { ThreadWasm &wasm_thread = static_cast<ThreadWasm &>(GetThread()); @@ -97,11 +112,7 @@ bool RegisterContextWasm::ReadRegister(const RegisterInfo *reg_info, const_cast<RegisterInfo *>(reg_info)); llvm::Expected<DataBufferSP> maybe_buffer = - wasm_reg_info->kind == eWasmTagGlobal - ? process->GetWasmGlobal(GetModuleID(), wasm_reg_info->index, - frame_index) - : process->GetWasmVariable(wasm_reg_info->kind, frame_index, - wasm_reg_info->index); + ReadWasmValue(*process, *wasm_reg_info, frame_index, GetModuleID()); if (!maybe_buffer) { LLDB_LOG_ERROR(GetLog(LLDBLog::Process), maybe_buffer.takeError(), "Failed to read Wasm value: {0}"); diff --git a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h index ea2cffe811130..dcb6e021435d0 100644 --- a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h +++ b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.h @@ -60,11 +60,10 @@ class RegisterContextWasm const RegisterValue &value) override; private: - /// The module whose code this context's frame is executing, which a virtual - /// register number has no room to carry. The frame's program counter is where - /// it comes from. Resolved on each use rather than held onto: the innermost - /// frame's context is the thread's own and outlives a stop, so a cached - /// answer would go on naming the module of a previous stop. + /// The module whose code this context's frame is executing. Resolved on each + /// use rather than cached: the innermost frame's register context is the + /// thread's own and outlives a stop, so a cached id would go on naming the + /// module of a previous stop. uint32_t GetModuleID(); std::unordered_map<size_t, std::unique_ptr<WasmVirtualRegisterInfo>> _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
