Jonas Devlieghere a28e7f1aad
[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
2025-07-29 10:07:13 -07:00

107 lines
3.4 KiB
Python

import os
import os.path
import lldb
from lldbsuite.test.lldbtest import *
from lldbsuite.test.gdbclientutils import *
class GDBRemoteTestBase(TestBase):
"""
Base class for GDB client tests.
This class will setup and start a mock GDB server for the test to use.
It also provides assertPacketLogContains, which simplifies the checking
of packets sent by the client.
"""
NO_DEBUG_INFO_TESTCASE = True
server = None
server_socket_class = TCPServerSocket
def setUp(self):
TestBase.setUp(self)
self.server = MockGDBServer(self.server_socket_class())
self.server.start()
def tearDown(self):
# TestBase.tearDown will kill the process, but we need to kill it early
# so its client connection closes and we can stop the server before
# finally calling the base tearDown.
if self.process() is not None:
self.process().Kill()
self.server.stop()
TestBase.tearDown(self)
def createTarget(self, yaml_path):
"""
Create a target by auto-generating the object based on the given yaml
instructions.
This will track the generated object so it can be automatically removed
during tearDown.
"""
yaml_base, ext = os.path.splitext(yaml_path)
obj_path = self.getBuildArtifact(yaml_base)
self.yaml2obj(yaml_path, obj_path)
return self.dbg.CreateTarget(obj_path)
def connect(self, target, plugin="gdb-remote"):
"""
Create a process by connecting to the mock GDB server.
Includes assertions that the process was successfully created.
"""
listener = self.dbg.GetListener()
error = lldb.SBError()
process = target.ConnectRemote(
listener, self.server.get_connect_url(), plugin, error
)
self.assertTrue(error.Success(), error.description)
self.assertTrue(process, PROCESS_IS_VALID)
return process
def assertPacketLogContains(self, packets, log=None):
"""
Assert that the mock server's packet log contains the given packets.
The packet log includes all packets sent by the client and received
by the server. This fuction makes it easy to verify that the client
sent the expected packets to the server.
The check does not require that the packets be consecutive, but does
require that they are ordered in the log as they ordered in the arg.
"""
if log is None:
log = self.server.responder.packetLog
i = 0
j = 0
while i < len(packets) and j < len(log):
if log[j] == packets[i]:
i += 1
j += 1
if i < len(packets):
self.fail(
"Did not receive: %s\nLast 10 packets:\n\t%s"
% (packets[i], "\n\t".join(log))
)
class GDBPlatformClientTestBase(GDBRemoteTestBase):
"""
Base class for platform server clients.
This class extends GDBRemoteTestBase by automatically connecting
via "platform connect" in the setUp() method.
"""
def setUp(self):
super().setUp()
self.runCmd("platform select remote-gdb-server")
self.runCmd("platform connect " + self.server.get_connect_url())
self.assertTrue(self.dbg.GetSelectedPlatform().IsConnected())
def tearDown(self):
self.dbg.GetSelectedPlatform().DisconnectRemote()
super().tearDown()