Skip to content

Backport Wasm Debugging changes to the LLVM 21.x Release #151559

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

Open
wants to merge 4 commits into
base: release/21.x
Choose a base branch
from

Conversation

JDevlieghere
Copy link
Member

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 llvm#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 llvm#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 llvm#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 llvm#1: 0x40000000000001e5 wasm32_args.wasm`main at test.c:12:12
    frame llvm#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

(cherry picked from commit a28e7f1)
Add support for DW_OP_WASM_location in DWARFExpression. This PR rebases
at https://yurydelendik.github.io/webassembly-dwarf/ and supported by
LLVM-based toolchains such as Clang, Swift, Emscripten, and Rust.

(cherry picked from commit c2548a8)
This PR implements a register context for Wasm, which uses virtual
registers to resolve Wasm local, globals and stack values. The registers
are used to implement supprot for `DW_OP_WASM_location` in the DWARF
expression evaluator (llvm#151010). This also adds a more comprehensive
test, showing that we can use this to show local variables.

(cherry picked from commit 1c70fa8)
@llvmbot
Copy link
Member

llvmbot commented Jul 31, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

#151439


Patch is 99.21 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/151559.diff

34 Files Affected:

  • (modified) lldb/docs/resources/lldbgdbremote.md (+67)
  • (modified) lldb/include/lldb/Expression/DWARFExpression.h (+8-4)
  • (modified) lldb/packages/Python/lldbsuite/test/lldbgdbclient.py (+2-2)
  • (modified) lldb/source/Expression/DWARFExpression.cpp (+6-4)
  • (modified) lldb/source/Plugins/ObjectFile/wasm/ObjectFileWasm.cpp (+5-1)
  • (modified) lldb/source/Plugins/Process/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp (+7-2)
  • (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h (+2)
  • (added) lldb/source/Plugins/Process/wasm/CMakeLists.txt (+11)
  • (added) lldb/source/Plugins/Process/wasm/ProcessWasm.cpp (+166)
  • (added) lldb/source/Plugins/Process/wasm/ProcessWasm.h (+99)
  • (added) lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp (+109)
  • (added) lldb/source/Plugins/Process/wasm/RegisterContextWasm.h (+69)
  • (added) lldb/source/Plugins/Process/wasm/ThreadWasm.cpp (+51)
  • (added) lldb/source/Plugins/Process/wasm/ThreadWasm.h (+41)
  • (added) lldb/source/Plugins/Process/wasm/UnwindWasm.cpp (+85)
  • (added) lldb/source/Plugins/Process/wasm/UnwindWasm.h (+51)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/DWARFUnit.cpp (+3-1)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/DWARFUnit.h (+5-3)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARF.cpp (+4)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARF.h (+3)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARFDwo.cpp (+5-3)
  • (modified) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileDWARFDwo.h (+2-1)
  • (added) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileWasm.cpp (+100)
  • (added) lldb/source/Plugins/SymbolFile/DWARF/SymbolFileWasm.h (+34)
  • (modified) lldb/source/Target/Platform.cpp (+7)
  • (added) lldb/source/Utility/WasmVirtualRegisters.h (+53)
  • (modified) lldb/test/API/functionalities/gdb_remote_client/TestWasm.py (+165-58)
  • (added) lldb/test/API/functionalities/gdb_remote_client/simple.c (+10)
  • (added) lldb/test/API/functionalities/gdb_remote_client/simple.yaml (+228)
  • (modified) lldb/unittests/Expression/CMakeLists.txt (+2)
  • (modified) lldb/unittests/Expression/DWARFExpressionTest.cpp (+428-152)
  • (modified) llvm/docs/ReleaseNotes.md (+2)
diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md
index 80c68091ecd07..36b95f1073ebc 100644
--- a/lldb/docs/resources/lldbgdbremote.md
+++ b/lldb/docs/resources/lldbgdbremote.md
@@ -2463,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.
diff --git a/lldb/include/lldb/Expression/DWARFExpression.h b/lldb/include/lldb/Expression/DWARFExpression.h
index 37853c0b5a8fc..8fcc5d37b91c9 100644
--- a/lldb/include/lldb/Expression/DWARFExpression.h
+++ b/lldb/include/lldb/Expression/DWARFExpression.h
@@ -52,10 +52,10 @@ class DWARFExpression {
     GetVendorDWARFOpcodeSize(const DataExtractor &data,
                              const lldb::offset_t data_offset,
                              const uint8_t op) const = 0;
-    virtual bool ParseVendorDWARFOpcode(uint8_t op,
-                                        const DataExtractor &opcodes,
-                                        lldb::offset_t &offset,
-                                        Stack &stack) const = 0;
+    virtual bool
+    ParseVendorDWARFOpcode(uint8_t op, const DataExtractor &opcodes,
+                           lldb::offset_t &offset, RegisterContext *reg_ctx,
+                           lldb::RegisterKind reg_kind, Stack &stack) const = 0;
 
     Delegate(const Delegate &) = delete;
     Delegate &operator=(const Delegate &) = delete;
@@ -163,6 +163,10 @@ class DWARFExpression {
 
   bool MatchesOperand(StackFrame &frame, const Instruction::Operand &op) const;
 
+  static llvm::Error ReadRegisterValueAsScalar(RegisterContext *reg_ctx,
+                                               lldb::RegisterKind reg_kind,
+                                               uint32_t reg_num, Value &value);
+
 private:
   /// A data extractor capable of reading opcode bytes
   DataExtractor m_data;
diff --git a/lldb/packages/Python/lldbsuite/test/lldbgdbclient.py b/lldb/packages/Python/lldbsuite/test/lldbgdbclient.py
index 459460b84fbae..599f7878e6edb 100644
--- a/lldb/packages/Python/lldbsuite/test/lldbgdbclient.py
+++ b/lldb/packages/Python/lldbsuite/test/lldbgdbclient.py
@@ -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.
 
@@ -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)
diff --git a/lldb/source/Expression/DWARFExpression.cpp b/lldb/source/Expression/DWARFExpression.cpp
index 52891fcefd68b..f6e3af2fe38ff 100644
--- a/lldb/source/Expression/DWARFExpression.cpp
+++ b/lldb/source/Expression/DWARFExpression.cpp
@@ -91,9 +91,10 @@ void DWARFExpression::SetRegisterKind(RegisterKind reg_kind) {
   m_reg_kind = reg_kind;
 }
 
-static llvm::Error ReadRegisterValueAsScalar(RegisterContext *reg_ctx,
-                                             lldb::RegisterKind reg_kind,
-                                             uint32_t reg_num, Value &value) {
+llvm::Error
+DWARFExpression::ReadRegisterValueAsScalar(RegisterContext *reg_ctx,
+                                           lldb::RegisterKind reg_kind,
+                                           uint32_t reg_num, Value &value) {
   if (reg_ctx == nullptr)
     return llvm::createStringError("no register context in frame");
 
@@ -2302,7 +2303,8 @@ llvm::Expected<Value> DWARFExpression::Evaluate(
 
     default:
       if (dwarf_cu) {
-        if (dwarf_cu->ParseVendorDWARFOpcode(op, opcodes, offset, stack)) {
+        if (dwarf_cu->ParseVendorDWARFOpcode(op, opcodes, offset, reg_ctx,
+                                             reg_kind, stack)) {
           break;
         }
       }
diff --git a/lldb/source/Plugins/ObjectFile/wasm/ObjectFileWasm.cpp b/lldb/source/Plugins/ObjectFile/wasm/ObjectFileWasm.cpp
index 67963a790a4fe..b1efd25949379 100644
--- a/lldb/source/Plugins/ObjectFile/wasm/ObjectFileWasm.cpp
+++ b/lldb/source/Plugins/ObjectFile/wasm/ObjectFileWasm.cpp
@@ -376,9 +376,13 @@ DataExtractor ObjectFileWasm::ReadImageData(offset_t offset, uint32_t size) {
         DataBufferSP buffer_sp(data_up.release());
         data.SetData(buffer_sp, 0, buffer_sp->GetByteSize());
       }
+    } else if (offset < m_data.GetByteSize()) {
+      size =
+          std::min(static_cast<uint64_t>(size), m_data.GetByteSize() - offset);
+      return DataExtractor(m_data.GetDataStart() + offset, size, GetByteOrder(),
+                           GetAddressByteSize());
     }
   }
-
   data.SetByteOrder(GetByteOrder());
   return data;
 }
diff --git a/lldb/source/Plugins/Process/CMakeLists.txt b/lldb/source/Plugins/Process/CMakeLists.txt
index bd9b1b86dbf13..3413360e975fb 100644
--- a/lldb/source/Plugins/Process/CMakeLists.txt
+++ b/lldb/source/Plugins/Process/CMakeLists.txt
@@ -29,3 +29,4 @@ add_subdirectory(elf-core)
 add_subdirectory(mach-core)
 add_subdirectory(minidump)
 add_subdirectory(FreeBSDKernel)
+add_subdirectory(wasm)
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index a2c34ddfc252e..14dfdec6a6f62 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -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 =
@@ -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 {
@@ -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);
     }
   }
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
index 7ae33837fd067..7c3dfb179a4b3 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -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.
diff --git a/lldb/source/Plugins/Process/wasm/CMakeLists.txt b/lldb/source/Plugins/Process/wasm/CMakeLists.txt
new file mode 100644
index 0000000000000..779b97ec90d08
--- /dev/null
+++ b/lldb/source/Plugins/Process/wasm/CMakeLists.txt
@@ -0,0 +1,11 @@
+add_lldb_library(lldbPluginProcessWasm PLUGIN
+  ProcessWasm.cpp
+  RegisterContextWasm.cpp
+  ThreadWasm.cpp
+  UnwindWasm.cpp
+
+  LINK_LIBS
+    lldbCore
+  LINK_COMPONENTS
+    Support
+  )
diff --git a/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp
new file mode 100644
index 0000000000000..4482677336ca4
--- /dev/null
+++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.cpp
@@ -0,0 +1,166 @@
+//===----------------------------------------------------------------------===//
+//
+// 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;
+}
+
+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);
+
+  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;
+}
diff --git a/lldb/source/Plugins/Process/wasm/ProcessWasm.h b/lldb/source/Plugins/Process/wasm/ProcessWasm.h
new file mode 100644
index 0000000000000..22effe7340ed7
--- /dev/null
+++ b/lldb/source/Plugins/Process/wasm/ProcessWasm.h
@@ -0,0 +1,99 @@
+//===----------------------------------------------------------------------===//
+//
+// 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"
+#include "Utility/WasmVirtualRegisters.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);
+
+  /// 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;
+  }
+
+  ProcessWasm(const ProcessWasm &);
+  const ProcessWasm &operator=(const ProcessWasm &) = delete;
+};
+
+} // namespace wasm
+} // namespace lldb_private
+
+#endif
diff --git a/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp
new file mode 100644
index 0000000000000..b4681718cb688
--- /dev/null
+++ b/lldb/source/Plugins/Process/wasm/RegisterContextWasm.cpp
@@ -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 li...
[truncated]

@tru tru added this to the LLVM 21.x Release milestone Aug 1, 2025
@github-project-automation github-project-automation bot moved this to Needs Triage in LLVM Release Status Aug 1, 2025
@tru tru moved this from Needs Triage to Needs Review in LLVM Release Status Aug 1, 2025
@tru
Copy link
Collaborator

tru commented Aug 1, 2025

Who can review this @JDevlieghere ?

@@ -326,6 +326,8 @@ Changes to LLDB
RISC-V code with `disassemble`'s `--byte` option.
* LLDB added native support for the Model Context Protocol (MCP). An MCP
server can be started with the `protocol-server start MCP` command.
* LLDB can now set breakpoints, show backtraces, and display variables when
debugging Wasm with supported runtimes (WAMR and V8).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should have mentioned this before, but, I think it would be good to add the connect command here. So that people know they have to select the wasm plugin explicitly.

@DavidSpickett
Copy link
Collaborator

I reviewed a lot of the changes on main but was not involved in writing them or the work that they are being written for (Swift compiled to WASM, I believe).

The release note is the best place to start to understand it:

  • LLDB can now set breakpoints, show backtraces, and display variables when
    debugging Wasm with supported runtimes (WAMR and V8).

We had some wasm support before but it was not usable as I understand it. These changes are not full support of wasm, but it will now do something useful with the runtimes mentioned. It's new features, but with a limited scope.

So for backport risk, there are:

  • New tests. The nice thing is these tests are simulated targets so they run everywhere. On Linaro's Linux and Windows bots, there have been no problems so far. The new tests have no new dependencies either.
  • Some changes to internal DWARF handling functions. Debug information is well tested, so I don't consider this a risk.
  • Lots of new code specifically for WASM. This code is built unconditionally but does not bring in any new dependencies to LLDB. At runtime, this code is doing nothing unless you opt into connecting to a WASM target (in fact, due to a bug, the user has to type out the wasm plugin name, you can't accidentally do this).

Is this a relatively large set of new features? Yes.
Is it quite isolated? Yes.
Does it change any user facing commands or LLDB API? No

On that basis I'm ok with backporting this.

If we want a second opinion, @labath did not review these changes on main, so might have a different take.

@JDevlieghere
Copy link
Member Author

Great write-up @DavidSpickett. For transparency, I'm totally fine not taking these for the release. We will take them for the next Swift stable branch, which is based on the LLVM 21 release branch, so LLVM deserves the right of first refusal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Needs Review
Development

Successfully merging this pull request may close these issues.

4 participants