Skip to content

[lldb] Add WebAssembly Process Plugin #150143

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 29, 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
16 changes: 16 additions & 0 deletions lldb/docs/resources/lldbgdbremote.md
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,22 @@ 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
4 changes: 2 additions & 2 deletions lldb/packages/Python/lldbsuite/test/lldbgdbclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def createTarget(self, yaml_path):
self.yaml2obj(yaml_path, obj_path)
return self.dbg.CreateTarget(obj_path)

def connect(self, target):
def connect(self, target, plugin="gdb-remote"):
"""
Create a process by connecting to the mock GDB server.

Expand All @@ -54,7 +54,7 @@ def connect(self, target):
listener = self.dbg.GetListener()
error = lldb.SBError()
process = target.ConnectRemote(
listener, self.server.get_connect_url(), "gdb-remote", error
listener, self.server.get_connect_url(), plugin, error
)
self.assertTrue(error.Success(), error.description)
self.assertTrue(process, PROCESS_IS_VALID)
Expand Down
1 change: 1 addition & 0 deletions lldb/source/Plugins/Process/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ add_subdirectory(elf-core)
add_subdirectory(mach-core)
add_subdirectory(minidump)
add_subdirectory(FreeBSDKernel)
add_subdirectory(wasm)
9 changes: 7 additions & 2 deletions lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ ProcessGDBRemote::~ProcessGDBRemote() {
KillDebugserverProcess();
}

std::shared_ptr<ThreadGDBRemote>
ProcessGDBRemote::CreateThread(lldb::tid_t tid) {
return std::make_shared<ThreadGDBRemote>(*this, tid);
}

bool ProcessGDBRemote::ParsePythonTargetDefinition(
const FileSpec &target_definition_fspec) {
ScriptInterpreter *interpreter =
Expand Down Expand Up @@ -1594,7 +1599,7 @@ bool ProcessGDBRemote::DoUpdateThreadList(ThreadList &old_thread_list,
ThreadSP thread_sp(
old_thread_list_copy.RemoveThreadByProtocolID(tid, false));
if (!thread_sp) {
thread_sp = std::make_shared<ThreadGDBRemote>(*this, tid);
thread_sp = CreateThread(tid);
LLDB_LOGV(log, "Making new thread: {0} for thread ID: {1:x}.",
thread_sp.get(), thread_sp->GetID());
} else {
Expand Down Expand Up @@ -1726,7 +1731,7 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(

if (!thread_sp) {
// Create the thread if we need to
thread_sp = std::make_shared<ThreadGDBRemote>(*this, tid);
thread_sp = CreateThread(tid);
m_thread_list_real.AddThread(thread_sp);
}
}
Expand Down
2 changes: 2 additions & 0 deletions lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ class ProcessGDBRemote : public Process,

ProcessGDBRemote(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp);

virtual std::shared_ptr<ThreadGDBRemote> CreateThread(lldb::tid_t tid);

bool SupportsMemoryTagging() override;

/// Broadcaster event bits definitions.
Expand Down
10 changes: 10 additions & 0 deletions lldb/source/Plugins/Process/wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
add_lldb_library(lldbPluginProcessWasm PLUGIN
ProcessWasm.cpp
ThreadWasm.cpp
UnwindWasm.cpp

LINK_LIBS
lldbCore
LINK_COMPONENTS
Support
)
133 changes: 133 additions & 0 deletions lldb/source/Plugins/Process/wasm/ProcessWasm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//===----------------------------------------------------------------------===//
//
// 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 "ProcessWasm.h"
#include "ThreadWasm.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Core/Value.h"
#include "lldb/Utility/DataBufferHeap.h"

#include "lldb/Target/UnixSignals.h"

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

LLDB_PLUGIN_DEFINE(ProcessWasm)

ProcessWasm::ProcessWasm(lldb::TargetSP target_sp, ListenerSP listener_sp)
: ProcessGDBRemote(target_sp, listener_sp) {
assert(target_sp);
// Wasm doesn't have any Unix-like signals as a platform concept, but pretend
// like it does to appease LLDB.
m_unix_signals_sp = UnixSignals::Create(target_sp->GetArchitecture());
}

void ProcessWasm::Initialize() {
static llvm::once_flag g_once_flag;

llvm::call_once(g_once_flag, []() {
PluginManager::RegisterPlugin(GetPluginNameStatic(),
GetPluginDescriptionStatic(), CreateInstance,
DebuggerInitialize);
});
}

void ProcessWasm::DebuggerInitialize(Debugger &debugger) {
ProcessGDBRemote::DebuggerInitialize(debugger);
}

llvm::StringRef ProcessWasm::GetPluginName() { return GetPluginNameStatic(); }

llvm::StringRef ProcessWasm::GetPluginNameStatic() { return "wasm"; }

llvm::StringRef ProcessWasm::GetPluginDescriptionStatic() {
return "GDB Remote protocol based WebAssembly debugging plug-in.";
}

void ProcessWasm::Terminate() {
PluginManager::UnregisterPlugin(ProcessWasm::CreateInstance);
}

lldb::ProcessSP ProcessWasm::CreateInstance(lldb::TargetSP target_sp,
ListenerSP listener_sp,
const FileSpec *crash_file_path,
bool can_connect) {
if (crash_file_path == nullptr)
return std::make_shared<ProcessWasm>(target_sp, listener_sp);
return {};
}

bool ProcessWasm::CanDebug(lldb::TargetSP target_sp,
bool plugin_specified_by_name) {
if (plugin_specified_by_name)
return true;

if (Module *exe_module = target_sp->GetExecutableModulePointer()) {
if (ObjectFile *exe_objfile = exe_module->GetObjectFile())
return exe_objfile->GetArchitecture().GetMachine() ==
llvm::Triple::wasm32;
}

// However, if there is no wasm module, we return false, otherwise,
// we might use ProcessWasm to attach gdb remote.
return false;
}

std::shared_ptr<ThreadGDBRemote> ProcessWasm::CreateThread(lldb::tid_t tid) {
return std::make_shared<ThreadWasm>(*this, tid);
}

size_t ProcessWasm::ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
Status &error) {
wasm_addr_t wasm_addr(vm_addr);

switch (wasm_addr.GetType()) {
case WasmAddressType::Memory:
case WasmAddressType::Object:
return ProcessGDBRemote::ReadMemory(vm_addr, buf, size, error);
case WasmAddressType::Invalid:
error.FromErrorStringWithFormat(
"Wasm read failed for invalid address 0x%" PRIx64, vm_addr);
return 0;
}
}

llvm::Expected<std::vector<lldb::addr_t>>
ProcessWasm::GetWasmCallStack(lldb::tid_t tid) {
StreamString packet;
packet.Printf("qWasmCallStack:");
packet.Printf("%llx", tid);

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

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

WritableDataBufferSP data_buffer_sp =
std::make_shared<DataBufferHeap>(response.GetStringRef().size() / 2, 0);
const size_t bytes = response.GetHexBytes(data_buffer_sp->GetData(), '\xcc');
if (bytes == 0 || bytes % sizeof(uint64_t) != 0)
return llvm::createStringError("invalid response for qWasmCallStack");

// To match the Wasm specification, the addresses are encoded in little endian
// byte order.
DataExtractor data(data_buffer_sp, lldb::eByteOrderLittle,
GetAddressByteSize());
lldb::offset_t offset = 0;
std::vector<lldb::addr_t> call_stack_pcs;
while (offset < bytes)
call_stack_pcs.push_back(data.GetU64(&offset));

return call_stack_pcs;
}
91 changes: 91 additions & 0 deletions lldb/source/Plugins/Process/wasm/ProcessWasm.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// 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_PROCESSWASM_H
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_PROCESSWASM_H

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

namespace lldb_private {
namespace wasm {

/// Each WebAssembly module has separated address spaces for Code and Memory.
/// A WebAssembly module also has a Data section which, when the module is
/// loaded, gets mapped into a region in the module Memory.
enum WasmAddressType : uint8_t { Memory = 0x00, Object = 0x01, Invalid = 0xff };

/// 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.
struct wasm_addr_t {
uint64_t offset : 32;
uint64_t module_id : 30;
uint64_t type : 2;

wasm_addr_t(lldb::addr_t addr)
: offset(addr & 0x00000000ffffffff),
module_id((addr & 0x00ffffff00000000) >> 32), type(addr >> 62) {}

wasm_addr_t(WasmAddressType type, uint32_t module_id, uint32_t offset)
: offset(offset), module_id(module_id), type(type) {}

WasmAddressType GetType() { return static_cast<WasmAddressType>(type); }

operator lldb::addr_t() { return *(uint64_t *)this; }
};

static_assert(sizeof(wasm_addr_t) == 8, "");

/// ProcessWasm provides the access to the Wasm program state
/// retrieved from the Wasm engine.
class ProcessWasm : public process_gdb_remote::ProcessGDBRemote {
public:
ProcessWasm(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp);
~ProcessWasm() override = default;

static lldb::ProcessSP CreateInstance(lldb::TargetSP target_sp,
lldb::ListenerSP listener_sp,
const FileSpec *crash_file_path,
bool can_connect);

static void Initialize();
static void DebuggerInitialize(Debugger &debugger);
static void Terminate();

static llvm::StringRef GetPluginNameStatic();
static llvm::StringRef GetPluginDescriptionStatic();

llvm::StringRef GetPluginName() override;

size_t ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
Status &error) override;

bool CanDebug(lldb::TargetSP target_sp,
bool plugin_specified_by_name) override;

/// Retrieve the current call stack from the WebAssembly remote process.
llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack(lldb::tid_t tid);

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

private:
friend class UnwindWasm;
process_gdb_remote::GDBRemoteDynamicRegisterInfoSP &GetRegisterInfo() {
return m_register_info_sp;
}

ProcessWasm(const ProcessWasm &);
const ProcessWasm &operator=(const ProcessWasm &) = delete;
};

} // namespace wasm
} // namespace lldb_private

#endif
34 changes: 34 additions & 0 deletions lldb/source/Plugins/Process/wasm/ThreadWasm.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
//
// 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 "ThreadWasm.h"

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

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

Unwind &ThreadWasm::GetUnwinder() {
if (!m_unwinder_up) {
assert(CalculateTarget()->GetArchitecture().GetMachine() ==
llvm::Triple::wasm32);
m_unwinder_up.reset(new wasm::UnwindWasm(*this));
}
return *m_unwinder_up;
}

llvm::Expected<std::vector<lldb::addr_t>> ThreadWasm::GetWasmCallStack() {
if (ProcessSP process_sp = GetProcess()) {
ProcessWasm *wasm_process = static_cast<ProcessWasm *>(process_sp.get());
return wasm_process->GetWasmCallStack(GetID());
}
return llvm::createStringError("no process");
}
38 changes: 38 additions & 0 deletions lldb/source/Plugins/Process/wasm/ThreadWasm.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// 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_THREADWASM_H
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_THREADWASM_H

#include "Plugins/Process/gdb-remote/ThreadGDBRemote.h"

namespace lldb_private {
namespace wasm {

/// ProcessWasm provides the access to the Wasm program state
/// retrieved from the Wasm engine.
class ThreadWasm : public process_gdb_remote::ThreadGDBRemote {
public:
ThreadWasm(Process &process, lldb::tid_t tid)
: process_gdb_remote::ThreadGDBRemote(process, tid) {}
~ThreadWasm() override = default;

/// Retrieve the current call stack from the WebAssembly remote process.
llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack();

protected:
Unwind &GetUnwinder() override;

ThreadWasm(const ThreadWasm &);
const ThreadWasm &operator=(const ThreadWasm &) = delete;
};

} // namespace wasm
} // namespace lldb_private

#endif // LLDB_SOURCE_PLUGINS_PROCESS_WASM_THREADWASM_H
Loading
Loading