Skip to content

[lldb] Implement RegisterContextWasm #151056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 67 additions & 16 deletions lldb/docs/resources/lldbgdbremote.md
Original file line number Diff line number Diff line change
Expand Up @@ -1998,22 +1998,6 @@ threads (live system debug) / cores (JTAG) in your program have
stopped and allows LLDB to display and control your program
correctly.

## qWasmCallStack

Get the Wasm call stack for the given thread id. This returns a hex-encoded
list of PC values, one for each frame of the call stack. To match the Wasm
specification, the addresses are encoded in little endian byte order, even if
the endian of the Wasm runtime's host is not little endian.

```
send packet: $qWasmCallStack:202dbe040#08
read packet: $9c01000000000040e501000000000040fe01000000000040#
```

**Priority to Implement:** Only required for Wasm support. This packed is
supported by the [WAMR](https://github.com/bytecodealliance/wasm-micro-runtime)
and [V8](https://v8.dev) Wasm runtimes.

## qWatchpointSupportInfo

Get the number of hardware watchpoints available on the remote target.
Expand Down Expand Up @@ -2479,3 +2463,70 @@ omitting them will work fine; these numbers are always base 16.

The length of the payload is not provided. A reliable, 8-bit clean,
transport layer is assumed.

## Wasm Packets

The packet below are supported by the
[WAMR](https://github.com/bytecodealliance/wasm-micro-runtime) and
[V8](https://v8.dev) Wasm runtimes.


### qWasmCallStack

Get the Wasm call stack for the given thread id. This returns a hex-encoded
list of PC values, one for each frame of the call stack. To match the Wasm
specification, the addresses are encoded in little endian byte order, even if
the endian of the Wasm runtime's host is not little endian.

```
send packet: $qWasmCallStack:202dbe040#08
read packet: $9c01000000000040e501000000000040fe01000000000040#
```

**Priority to Implement:** Only required for Wasm support. Necessary to show
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
address from where to read the value.

```
send packet: $qWasmGlobal:0;2#cb
read packet: $e0030100#b9
```

**Priority to Implement:** Only required for Wasm support. Necessary to show
variables.


### qWasmLocal

Get the value of a Wasm function argument or local variable for the given frame
index at the given variable index. The indexes are encoded as base 10. The
result is a hex-encoded address from where to read the value.


```
send packet: $qWasmLocal:0;2#cb
read packet: $e0030100#b9
```

**Priority to Implement:** Only required for Wasm support. Necessary to show
variables.


### qWasmStackValue

Get the value of a Wasm local variable from the Wasm operand stack, for the
given frame index at the given variable index. The indexes are encoded as base
10. The result is a hex-encoded address from where to read value.

```
send packet: $qWasmStackValue:0;2#cb
read packet: $e0030100#b9
```

**Priority to Implement:** Only required for Wasm support. Necessary to show
variables.
1 change: 1 addition & 0 deletions lldb/source/Plugins/Process/wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_lldb_library(lldbPluginProcessWasm PLUGIN
ProcessWasm.cpp
RegisterContextWasm.cpp
ThreadWasm.cpp
UnwindWasm.cpp

Expand Down
33 changes: 33 additions & 0 deletions lldb/source/Plugins/Process/wasm/ProcessWasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,36 @@ ProcessWasm::GetWasmCallStack(lldb::tid_t tid) {

return call_stack_pcs;
}

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);
Copy link
Collaborator

@jasonmolenda jasonmolenda Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these fields sent in base10 or base16? I wasn't clear from the packet description in the doc. same thing for qWasmGlobal and qWasmLocal. (I'd expect to see a base16 without a 0x prefix in a gdb-remote packet for a field like this)
(I've tried to sneak some 0x's in to gdb-remote packets but Pavel always notices and slaps my hand. it's easy to not see the base when you're testing with small numbers.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This impl https://github.com/bytecodealliance/wasm-micro-runtime/blob/23799a2cb6778896e5e7797d17bc826e1afdbf6e/core/iwasm/libraries/debug-engine/handler.c#L165
seems to be expecting base10. quite unusual for the gdb remote serial protocol, but what you gonna do, if that's the choice they made.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our doc is now clear on this topic.

Feels like they encoded it based on how easy it was to read each bit. Indexes are usually numbers and addresses are better in hex? It is what it is anyway.


StringExtractorGDBRemote response;
if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) !=
GDBRemoteCommunication::PacketResult::Success)
return llvm::createStringError("failed to send Wasm variable");

if (!response.IsNormalResponse())
return llvm::createStringError("failed to get response for Wasm variable");

WritableDataBufferSP buffer_sp(
new DataBufferHeap(response.GetStringRef().size() / 2, 0));
response.GetHexBytes(buffer_sp->GetData(), '\xcc');
return buffer_sp;
}
8 changes: 8 additions & 0 deletions lldb/source/Plugins/Process/wasm/ProcessWasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_PROCESSWASM_H

#include "Plugins/Process/gdb-remote/ProcessGDBRemote.h"
#include "Utility/WasmVirtualRegisters.h"

namespace lldb_private {
namespace wasm {
Expand Down Expand Up @@ -71,12 +72,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.
llvm::Expected<lldb::DataBufferSP>
GetWasmVariable(WasmVirtualRegisterKinds kind, int frame_index, int index);

protected:
std::shared_ptr<process_gdb_remote::ThreadGDBRemote>
CreateThread(lldb::tid_t tid) override;

private:
friend class UnwindWasm;
friend class ThreadWasm;

process_gdb_remote::GDBRemoteDynamicRegisterInfoSP &GetRegisterInfo() {
return m_register_info_sp;
}
Expand Down
109 changes: 109 additions & 0 deletions lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "RegisterContextWasm.h"
#include "Plugins/Process/gdb-remote/GDBRemoteRegisterContext.h"
#include "ProcessWasm.h"
#include "ThreadWasm.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/RegisterValue.h"
#include "llvm/Support/Error.h"
#include <memory>

using namespace lldb;
using namespace lldb_private;
using namespace lldb_private::process_gdb_remote;
using namespace lldb_private::wasm;

RegisterContextWasm::RegisterContextWasm(
wasm::ThreadWasm &thread, uint32_t concrete_frame_idx,
GDBRemoteDynamicRegisterInfoSP reg_info_sp)
: GDBRemoteRegisterContext(thread, concrete_frame_idx, reg_info_sp, false,
false) {}

RegisterContextWasm::~RegisterContextWasm() = default;

uint32_t RegisterContextWasm::ConvertRegisterKindToRegisterNumber(
lldb::RegisterKind kind, uint32_t num) {
return num;
}

size_t RegisterContextWasm::GetRegisterCount() {
// Wasm has no registers.
return 0;
}

const RegisterInfo *RegisterContextWasm::GetRegisterInfoAtIndex(size_t reg) {
uint32_t tag = GetWasmVirtualRegisterTag(reg);
if (tag == eWasmTagNotAWasmLocation)
return m_reg_info_sp->GetRegisterInfoAtIndex(
GetWasmVirtualRegisterIndex(reg));

auto it = m_register_map.find(reg);
if (it == m_register_map.end()) {
WasmVirtualRegisterKinds kind = static_cast<WasmVirtualRegisterKinds>(tag);
std::tie(it, std::ignore) = m_register_map.insert(
{reg, std::make_unique<WasmVirtualRegisterInfo>(
kind, GetWasmVirtualRegisterIndex(reg))});
}
return it->second.get();
}

size_t RegisterContextWasm::GetRegisterSetCount() { return 0; }

const RegisterSet *RegisterContextWasm::GetRegisterSet(size_t reg_set) {
// Wasm has no registers.
return nullptr;
}

bool RegisterContextWasm::ReadRegister(const RegisterInfo *reg_info,
RegisterValue &value) {
// The only real registers is the PC.
if (reg_info->name)
return GDBRemoteRegisterContext::ReadRegister(reg_info, value);

// Read the virtual registers.
ThreadWasm *thread = static_cast<ThreadWasm *>(&GetThread());
ProcessWasm *process = static_cast<ProcessWasm *>(thread->GetProcess().get());
if (!thread)
return false;

uint32_t frame_index = m_concrete_frame_idx;
WasmVirtualRegisterInfo *wasm_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);
if (!maybe_buffer) {
LLDB_LOG_ERROR(GetLog(LLDBLog::Process), maybe_buffer.takeError(),
"Failed to read Wasm local: {0}");
return false;
}

DataBufferSP buffer_sp = *maybe_buffer;
DataExtractor reg_data(buffer_sp, process->GetByteOrder(),
process->GetAddressByteSize());
wasm_reg_info->byte_size = buffer_sp->GetByteSize();
wasm_reg_info->encoding = lldb::eEncodingUint;

Status error = value.SetValueFromData(
*reg_info, reg_data, reg_info->byte_offset, /*partial_data_ok=*/false);
return error.Success();
}

void RegisterContextWasm::InvalidateAllRegisters() {}

bool RegisterContextWasm::WriteRegister(const RegisterInfo *reg_info,
const RegisterValue &value) {
// The only real registers is the PC.
if (reg_info->name)
return GDBRemoteRegisterContext::WriteRegister(reg_info, value);
return false;
}
69 changes: 69 additions & 0 deletions lldb/source/Plugins/Process/wasm/RegisterContextWasm.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLDB_SOURCE_PLUGINS_PROCESS_WASM_REGISTERCONTEXTWASM_H
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_REGISTERCONTEXTWASM_H

#include "Plugins/Process/gdb-remote/GDBRemoteRegisterContext.h"
#include "ThreadWasm.h"
#include "Utility/WasmVirtualRegisters.h"
#include "lldb/lldb-private-types.h"
#include <unordered_map>

namespace lldb_private {
namespace wasm {

class RegisterContextWasm;

typedef std::shared_ptr<RegisterContextWasm> RegisterContextWasmSP;

struct WasmVirtualRegisterInfo : public RegisterInfo {
WasmVirtualRegisterKinds kind;
uint32_t index;

WasmVirtualRegisterInfo(WasmVirtualRegisterKinds kind, uint32_t index)
: RegisterInfo(), kind(kind), index(index) {}
};

class RegisterContextWasm
: public process_gdb_remote::GDBRemoteRegisterContext {
public:
RegisterContextWasm(
wasm::ThreadWasm &thread, uint32_t concrete_frame_idx,
process_gdb_remote::GDBRemoteDynamicRegisterInfoSP reg_info_sp);

~RegisterContextWasm() override;

uint32_t ConvertRegisterKindToRegisterNumber(lldb::RegisterKind kind,
uint32_t num) override;

void InvalidateAllRegisters() override;

size_t GetRegisterCount() override;

const RegisterInfo *GetRegisterInfoAtIndex(size_t reg) override;

size_t GetRegisterSetCount() override;

const RegisterSet *GetRegisterSet(size_t reg_set) override;

bool ReadRegister(const RegisterInfo *reg_info,
RegisterValue &value) override;

bool WriteRegister(const RegisterInfo *reg_info,
const RegisterValue &value) override;

private:
std::unordered_map<size_t, std::unique_ptr<WasmVirtualRegisterInfo>>
m_register_map;
};

} // namespace wasm
} // namespace lldb_private

#endif
17 changes: 17 additions & 0 deletions lldb/source/Plugins/Process/wasm/ThreadWasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "ThreadWasm.h"

#include "ProcessWasm.h"
#include "RegisterContextWasm.h"
#include "UnwindWasm.h"
#include "lldb/Target/Target.h"

Expand All @@ -32,3 +33,19 @@ llvm::Expected<std::vector<lldb::addr_t>> ThreadWasm::GetWasmCallStack() {
}
return llvm::createStringError("no process");
}

lldb::RegisterContextSP
ThreadWasm::CreateRegisterContextForFrame(StackFrame *frame) {
uint32_t concrete_frame_idx = 0;
ProcessSP process_sp(GetProcess());
ProcessWasm *wasm_process = static_cast<ProcessWasm *>(process_sp.get());

if (frame)
concrete_frame_idx = frame->GetConcreteFrameIndex();

if (concrete_frame_idx == 0)
return std::make_shared<RegisterContextWasm>(
*this, concrete_frame_idx, wasm_process->GetRegisterInfo());

return GetUnwinder().CreateRegisterContextForFrame(frame);
}
3 changes: 3 additions & 0 deletions lldb/source/Plugins/Process/wasm/ThreadWasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ThreadWasm : public process_gdb_remote::ThreadGDBRemote {
/// Retrieve the current call stack from the WebAssembly remote process.
llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack();

lldb::RegisterContextSP
CreateRegisterContextForFrame(StackFrame *frame) override;

protected:
Unwind &GetUnwinder() override;

Expand Down
Loading
Loading