Skip to content

Commit a28e7f1

Browse files
authored
[lldb] Add WebAssembly Process Plugin (#150143)
Extend support in LLDB for WebAssembly. This PR adds a new Process plugin (ProcessWasm) that extends ProcessGDBRemote for WebAssembly targets. It adds support for WebAssembly's memory model with separate address spaces, and the ability to fetch the call stack from the WebAssembly runtime. I have tested this change with the WebAssembly Micro Runtime (WAMR, https://github.com/bytecodealliance/wasm-micro-runtime) which implements a GDB debug stub and supports the qWasmCallStack packet. ``` (lldb) process connect --plugin wasm connect://localhost:4567 Process 1 stopped * thread #1, name = 'nobody', stop reason = trace frame #0: 0x40000000000001ad wasm32_args.wasm`main: -> 0x40000000000001ad <+3>: global.get 0 0x40000000000001b3 <+9>: i32.const 16 0x40000000000001b5 <+11>: i32.sub 0x40000000000001b6 <+12>: local.set 0 (lldb) b add Breakpoint 1: where = wasm32_args.wasm`add + 28 at test.c:4:12, address = 0x400000000000019c (lldb) c Process 1 resuming Process 1 stopped * thread #1, name = 'nobody', stop reason = breakpoint 1.1 frame #0: 0x400000000000019c wasm32_args.wasm`add(a=<unavailable>, b=<unavailable>) at test.c:4:12 1 int 2 add(int a, int b) 3 { -> 4 return a + b; 5 } 6 7 int (lldb) bt * thread #1, name = 'nobody', stop reason = breakpoint 1.1 * frame #0: 0x400000000000019c wasm32_args.wasm`add(a=<unavailable>, b=<unavailable>) at test.c:4:12 frame #1: 0x40000000000001e5 wasm32_args.wasm`main at test.c:12:12 frame #2: 0x40000000000001fe wasm32_args.wasm ``` This PR is based on an unmerged patch from Paolo Severini: https://reviews.llvm.org/D78801. I intentionally stuck to the foundations to keep this PR small. I have more PRs in the pipeline to support the other features/packets. My motivation for supporting Wasm is to support debugging Swift compiled to WebAssembly: https://www.swift.org/documentation/articles/wasm-getting-started.html
1 parent 4128cf3 commit a28e7f1

File tree

14 files changed

+499
-10
lines changed

14 files changed

+499
-10
lines changed

lldb/docs/resources/lldbgdbremote.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1998,6 +1998,22 @@ threads (live system debug) / cores (JTAG) in your program have
19981998
stopped and allows LLDB to display and control your program
19991999
correctly.
20002000
2001+
## qWasmCallStack
2002+
2003+
Get the Wasm call stack for the given thread id. This returns a hex-encoded
2004+
list of PC values, one for each frame of the call stack. To match the Wasm
2005+
specification, the addresses are encoded in little endian byte order, even if
2006+
the endian of the Wasm runtime's host is not little endian.
2007+
2008+
```
2009+
send packet: $qWasmCallStack:202dbe040#08
2010+
read packet: $9c01000000000040e501000000000040fe01000000000040#
2011+
```
2012+
2013+
**Priority to Implement:** Only required for Wasm support. This packed is
2014+
supported by the [WAMR](https://github.com/bytecodealliance/wasm-micro-runtime)
2015+
and [V8](https://v8.dev) Wasm runtimes.
2016+
20012017
## qWatchpointSupportInfo
20022018
20032019
Get the number of hardware watchpoints available on the remote target.

lldb/packages/Python/lldbsuite/test/lldbgdbclient.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def createTarget(self, yaml_path):
4545
self.yaml2obj(yaml_path, obj_path)
4646
return self.dbg.CreateTarget(obj_path)
4747

48-
def connect(self, target):
48+
def connect(self, target, plugin="gdb-remote"):
4949
"""
5050
Create a process by connecting to the mock GDB server.
5151
@@ -54,7 +54,7 @@ def connect(self, target):
5454
listener = self.dbg.GetListener()
5555
error = lldb.SBError()
5656
process = target.ConnectRemote(
57-
listener, self.server.get_connect_url(), "gdb-remote", error
57+
listener, self.server.get_connect_url(), plugin, error
5858
)
5959
self.assertTrue(error.Success(), error.description)
6060
self.assertTrue(process, PROCESS_IS_VALID)

lldb/source/Plugins/Process/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ add_subdirectory(elf-core)
2929
add_subdirectory(mach-core)
3030
add_subdirectory(minidump)
3131
add_subdirectory(FreeBSDKernel)
32+
add_subdirectory(wasm)

lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ ProcessGDBRemote::~ProcessGDBRemote() {
323323
KillDebugserverProcess();
324324
}
325325

326+
std::shared_ptr<ThreadGDBRemote>
327+
ProcessGDBRemote::CreateThread(lldb::tid_t tid) {
328+
return std::make_shared<ThreadGDBRemote>(*this, tid);
329+
}
330+
326331
bool ProcessGDBRemote::ParsePythonTargetDefinition(
327332
const FileSpec &target_definition_fspec) {
328333
ScriptInterpreter *interpreter =
@@ -1594,7 +1599,7 @@ bool ProcessGDBRemote::DoUpdateThreadList(ThreadList &old_thread_list,
15941599
ThreadSP thread_sp(
15951600
old_thread_list_copy.RemoveThreadByProtocolID(tid, false));
15961601
if (!thread_sp) {
1597-
thread_sp = std::make_shared<ThreadGDBRemote>(*this, tid);
1602+
thread_sp = CreateThread(tid);
15981603
LLDB_LOGV(log, "Making new thread: {0} for thread ID: {1:x}.",
15991604
thread_sp.get(), thread_sp->GetID());
16001605
} else {
@@ -1726,7 +1731,7 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
17261731

17271732
if (!thread_sp) {
17281733
// Create the thread if we need to
1729-
thread_sp = std::make_shared<ThreadGDBRemote>(*this, tid);
1734+
thread_sp = CreateThread(tid);
17301735
m_thread_list_real.AddThread(thread_sp);
17311736
}
17321737
}

lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ class ProcessGDBRemote : public Process,
246246

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

249+
virtual std::shared_ptr<ThreadGDBRemote> CreateThread(lldb::tid_t tid);
250+
249251
bool SupportsMemoryTagging() override;
250252

251253
/// Broadcaster event bits definitions.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
add_lldb_library(lldbPluginProcessWasm PLUGIN
2+
ProcessWasm.cpp
3+
ThreadWasm.cpp
4+
UnwindWasm.cpp
5+
6+
LINK_LIBS
7+
lldbCore
8+
LINK_COMPONENTS
9+
Support
10+
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "ProcessWasm.h"
10+
#include "ThreadWasm.h"
11+
#include "lldb/Core/Module.h"
12+
#include "lldb/Core/PluginManager.h"
13+
#include "lldb/Core/Value.h"
14+
#include "lldb/Utility/DataBufferHeap.h"
15+
16+
#include "lldb/Target/UnixSignals.h"
17+
18+
using namespace lldb;
19+
using namespace lldb_private;
20+
using namespace lldb_private::process_gdb_remote;
21+
using namespace lldb_private::wasm;
22+
23+
LLDB_PLUGIN_DEFINE(ProcessWasm)
24+
25+
ProcessWasm::ProcessWasm(lldb::TargetSP target_sp, ListenerSP listener_sp)
26+
: ProcessGDBRemote(target_sp, listener_sp) {
27+
assert(target_sp);
28+
// Wasm doesn't have any Unix-like signals as a platform concept, but pretend
29+
// like it does to appease LLDB.
30+
m_unix_signals_sp = UnixSignals::Create(target_sp->GetArchitecture());
31+
}
32+
33+
void ProcessWasm::Initialize() {
34+
static llvm::once_flag g_once_flag;
35+
36+
llvm::call_once(g_once_flag, []() {
37+
PluginManager::RegisterPlugin(GetPluginNameStatic(),
38+
GetPluginDescriptionStatic(), CreateInstance,
39+
DebuggerInitialize);
40+
});
41+
}
42+
43+
void ProcessWasm::DebuggerInitialize(Debugger &debugger) {
44+
ProcessGDBRemote::DebuggerInitialize(debugger);
45+
}
46+
47+
llvm::StringRef ProcessWasm::GetPluginName() { return GetPluginNameStatic(); }
48+
49+
llvm::StringRef ProcessWasm::GetPluginNameStatic() { return "wasm"; }
50+
51+
llvm::StringRef ProcessWasm::GetPluginDescriptionStatic() {
52+
return "GDB Remote protocol based WebAssembly debugging plug-in.";
53+
}
54+
55+
void ProcessWasm::Terminate() {
56+
PluginManager::UnregisterPlugin(ProcessWasm::CreateInstance);
57+
}
58+
59+
lldb::ProcessSP ProcessWasm::CreateInstance(lldb::TargetSP target_sp,
60+
ListenerSP listener_sp,
61+
const FileSpec *crash_file_path,
62+
bool can_connect) {
63+
if (crash_file_path == nullptr)
64+
return std::make_shared<ProcessWasm>(target_sp, listener_sp);
65+
return {};
66+
}
67+
68+
bool ProcessWasm::CanDebug(lldb::TargetSP target_sp,
69+
bool plugin_specified_by_name) {
70+
if (plugin_specified_by_name)
71+
return true;
72+
73+
if (Module *exe_module = target_sp->GetExecutableModulePointer()) {
74+
if (ObjectFile *exe_objfile = exe_module->GetObjectFile())
75+
return exe_objfile->GetArchitecture().GetMachine() ==
76+
llvm::Triple::wasm32;
77+
}
78+
79+
// However, if there is no wasm module, we return false, otherwise,
80+
// we might use ProcessWasm to attach gdb remote.
81+
return false;
82+
}
83+
84+
std::shared_ptr<ThreadGDBRemote> ProcessWasm::CreateThread(lldb::tid_t tid) {
85+
return std::make_shared<ThreadWasm>(*this, tid);
86+
}
87+
88+
size_t ProcessWasm::ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
89+
Status &error) {
90+
wasm_addr_t wasm_addr(vm_addr);
91+
92+
switch (wasm_addr.GetType()) {
93+
case WasmAddressType::Memory:
94+
case WasmAddressType::Object:
95+
return ProcessGDBRemote::ReadMemory(vm_addr, buf, size, error);
96+
case WasmAddressType::Invalid:
97+
error.FromErrorStringWithFormat(
98+
"Wasm read failed for invalid address 0x%" PRIx64, vm_addr);
99+
return 0;
100+
}
101+
}
102+
103+
llvm::Expected<std::vector<lldb::addr_t>>
104+
ProcessWasm::GetWasmCallStack(lldb::tid_t tid) {
105+
StreamString packet;
106+
packet.Printf("qWasmCallStack:");
107+
packet.Printf("%llx", tid);
108+
109+
StringExtractorGDBRemote response;
110+
if (m_gdb_comm.SendPacketAndWaitForResponse(packet.GetString(), response) !=
111+
GDBRemoteCommunication::PacketResult::Success)
112+
return llvm::createStringError("failed to send qWasmCallStack");
113+
114+
if (!response.IsNormalResponse())
115+
return llvm::createStringError("failed to get response for qWasmCallStack");
116+
117+
WritableDataBufferSP data_buffer_sp =
118+
std::make_shared<DataBufferHeap>(response.GetStringRef().size() / 2, 0);
119+
const size_t bytes = response.GetHexBytes(data_buffer_sp->GetData(), '\xcc');
120+
if (bytes == 0 || bytes % sizeof(uint64_t) != 0)
121+
return llvm::createStringError("invalid response for qWasmCallStack");
122+
123+
// To match the Wasm specification, the addresses are encoded in little endian
124+
// byte order.
125+
DataExtractor data(data_buffer_sp, lldb::eByteOrderLittle,
126+
GetAddressByteSize());
127+
lldb::offset_t offset = 0;
128+
std::vector<lldb::addr_t> call_stack_pcs;
129+
while (offset < bytes)
130+
call_stack_pcs.push_back(data.GetU64(&offset));
131+
132+
return call_stack_pcs;
133+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLDB_SOURCE_PLUGINS_PROCESS_WASM_PROCESSWASM_H
10+
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_PROCESSWASM_H
11+
12+
#include "Plugins/Process/gdb-remote/ProcessGDBRemote.h"
13+
14+
namespace lldb_private {
15+
namespace wasm {
16+
17+
/// Each WebAssembly module has separated address spaces for Code and Memory.
18+
/// A WebAssembly module also has a Data section which, when the module is
19+
/// loaded, gets mapped into a region in the module Memory.
20+
enum WasmAddressType : uint8_t { Memory = 0x00, Object = 0x01, Invalid = 0xff };
21+
22+
/// For the purpose of debugging, we can represent all these separated 32-bit
23+
/// address spaces with a single virtual 64-bit address space. The
24+
/// wasm_addr_t provides this encoding using bitfields.
25+
struct wasm_addr_t {
26+
uint64_t offset : 32;
27+
uint64_t module_id : 30;
28+
uint64_t type : 2;
29+
30+
wasm_addr_t(lldb::addr_t addr)
31+
: offset(addr & 0x00000000ffffffff),
32+
module_id((addr & 0x00ffffff00000000) >> 32), type(addr >> 62) {}
33+
34+
wasm_addr_t(WasmAddressType type, uint32_t module_id, uint32_t offset)
35+
: offset(offset), module_id(module_id), type(type) {}
36+
37+
WasmAddressType GetType() { return static_cast<WasmAddressType>(type); }
38+
39+
operator lldb::addr_t() { return *(uint64_t *)this; }
40+
};
41+
42+
static_assert(sizeof(wasm_addr_t) == 8, "");
43+
44+
/// ProcessWasm provides the access to the Wasm program state
45+
/// retrieved from the Wasm engine.
46+
class ProcessWasm : public process_gdb_remote::ProcessGDBRemote {
47+
public:
48+
ProcessWasm(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp);
49+
~ProcessWasm() override = default;
50+
51+
static lldb::ProcessSP CreateInstance(lldb::TargetSP target_sp,
52+
lldb::ListenerSP listener_sp,
53+
const FileSpec *crash_file_path,
54+
bool can_connect);
55+
56+
static void Initialize();
57+
static void DebuggerInitialize(Debugger &debugger);
58+
static void Terminate();
59+
60+
static llvm::StringRef GetPluginNameStatic();
61+
static llvm::StringRef GetPluginDescriptionStatic();
62+
63+
llvm::StringRef GetPluginName() override;
64+
65+
size_t ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
66+
Status &error) override;
67+
68+
bool CanDebug(lldb::TargetSP target_sp,
69+
bool plugin_specified_by_name) override;
70+
71+
/// Retrieve the current call stack from the WebAssembly remote process.
72+
llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack(lldb::tid_t tid);
73+
74+
protected:
75+
std::shared_ptr<process_gdb_remote::ThreadGDBRemote>
76+
CreateThread(lldb::tid_t tid) override;
77+
78+
private:
79+
friend class UnwindWasm;
80+
process_gdb_remote::GDBRemoteDynamicRegisterInfoSP &GetRegisterInfo() {
81+
return m_register_info_sp;
82+
}
83+
84+
ProcessWasm(const ProcessWasm &);
85+
const ProcessWasm &operator=(const ProcessWasm &) = delete;
86+
};
87+
88+
} // namespace wasm
89+
} // namespace lldb_private
90+
91+
#endif
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "ThreadWasm.h"
10+
11+
#include "ProcessWasm.h"
12+
#include "UnwindWasm.h"
13+
#include "lldb/Target/Target.h"
14+
15+
using namespace lldb;
16+
using namespace lldb_private;
17+
using namespace lldb_private::wasm;
18+
19+
Unwind &ThreadWasm::GetUnwinder() {
20+
if (!m_unwinder_up) {
21+
assert(CalculateTarget()->GetArchitecture().GetMachine() ==
22+
llvm::Triple::wasm32);
23+
m_unwinder_up.reset(new wasm::UnwindWasm(*this));
24+
}
25+
return *m_unwinder_up;
26+
}
27+
28+
llvm::Expected<std::vector<lldb::addr_t>> ThreadWasm::GetWasmCallStack() {
29+
if (ProcessSP process_sp = GetProcess()) {
30+
ProcessWasm *wasm_process = static_cast<ProcessWasm *>(process_sp.get());
31+
return wasm_process->GetWasmCallStack(GetID());
32+
}
33+
return llvm::createStringError("no process");
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLDB_SOURCE_PLUGINS_PROCESS_WASM_THREADWASM_H
10+
#define LLDB_SOURCE_PLUGINS_PROCESS_WASM_THREADWASM_H
11+
12+
#include "Plugins/Process/gdb-remote/ThreadGDBRemote.h"
13+
14+
namespace lldb_private {
15+
namespace wasm {
16+
17+
/// ProcessWasm provides the access to the Wasm program state
18+
/// retrieved from the Wasm engine.
19+
class ThreadWasm : public process_gdb_remote::ThreadGDBRemote {
20+
public:
21+
ThreadWasm(Process &process, lldb::tid_t tid)
22+
: process_gdb_remote::ThreadGDBRemote(process, tid) {}
23+
~ThreadWasm() override = default;
24+
25+
/// Retrieve the current call stack from the WebAssembly remote process.
26+
llvm::Expected<std::vector<lldb::addr_t>> GetWasmCallStack();
27+
28+
protected:
29+
Unwind &GetUnwinder() override;
30+
31+
ThreadWasm(const ThreadWasm &);
32+
const ThreadWasm &operator=(const ThreadWasm &) = delete;
33+
};
34+
35+
} // namespace wasm
36+
} // namespace lldb_private
37+
38+
#endif // LLDB_SOURCE_PLUGINS_PROCESS_WASM_THREADWASM_H

0 commit comments

Comments
 (0)