[Lldb-commits] [lldb] [lldb] Scope Wasm global reads to a module instance (PR #213176)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Mon Aug 3 16:48:22 PDT 2026
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/213176
>From 16596124dfd6998efc1fc95f73e15f1ae3225763 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 30 Jul 2026 17:00:49 -0700
Subject: [PATCH] [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 | 687 +++++++++++++++---
.../simple_global_frame_base.yaml | 238 ++++++
11 files changed, 1061 insertions(+), 131 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 e208c16649832..f4fc664e67837 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
@@ -231,6 +231,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
@@ -429,6 +435,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
@@ -458,6 +465,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
@@ -525,6 +533,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 e119b3e3ecf6d..a803c953db1b2 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"
@@ -88,24 +89,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;
@@ -125,8 +116,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(lldb::addr_t vm_addr, void *buf, size_t size,
Status &error) {
+ // A caller may reuse one error across reads, as the overridden
+ // Process::ReadMemory allows.
+ error.Clear();
+
wasm_addr_t wasm_addr(vm_addr);
switch (wasm_addr.GetType()) {
@@ -181,34 +192,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 9bce07ec5691c..28ba162333a6f 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 39b784315a9c1..44aaad5fc9894 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 861691f5b3ddc..43c1bac02f5a0 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.
+ """
+
+ 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()
+
-# Globals the fake engine holds, as index -> (size in bytes, value).
-WASM_GLOBALS = {0: (4, 0x2A), 1: (8, 0xDEADBEEF)}
+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("")
@@ -406,20 +531,29 @@ def test_simple_wasm_debugging_session(self):
self.assertTrue(b.IsValid())
self.assertEqual(b.GetValueAsUnsigned(), 2)
- @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")
@@ -427,14 +561,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()
@@ -446,6 +659,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)
@@ -462,3 +679,311 @@ def test_read_global(self):
# Likewise for a global that does not exist.
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"])
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
+...
More information about the lldb-commits
mailing list