This post describes how to debug WebAssembly code running under the WebAssembly Micro Runtime (WAMR) on macOS. WAMR is a lightweight WebAssembly runtime designed for embedded and IoT applications, but its simplicity and debugging support make it well suited for general development.

Building the WebAssembly Micro Runtime

We have to build the runtime with debugging support. Start by cloning the wasm-micro-runtime repository and optionally check out a specific release.

$ git clone [email protected]:bytecodealliance/wasm-micro-runtime.git
$ cd wasm-micro-runtime
$ git checkout release/2.4.x

Next, build iwasm. This is the binary that uses WAMR VMcore and allows us to interact with it from the command line. We need to enable debugging support (WAMR_BUILD_DEBUG_INTERP=1) and specify the target architecture (WAMR_BUILD_TARGET=AARCH64 for Apple Silicon). I’m using Ninja as the generator since that’s what I’m used to when building LLVM and LLDB.

$ cd product-mini/platforms/darwin/
$ mkdir build && cd build
$ cmake .. -DWAMR_BUILD_DEBUG_INTERP=1 -DWAMR_BUILD_TARGET=AARCH64 -G Ninja
$ ninja

Building a test program

If you already have a test program, you can skip this section. Here’s a simple test program (simple.c) with no dependencies:

int add(int a, int b) {
  return a + b;
}

int main() {
  int i = 1;
  int j = 2;
  return add(i, j);
}

You can compile it directly with clang if it was built with Wasm support.

$ clang -target wasm32 -nostdlib -Wl,--no-entry -Wl,--export-all -O0 -g -o simple.wasm simple.c

We omitted any includes so we can build without a standard library. We tell the linker not to expect an entry symbol (like _start) and to export everything. We’re building without optimizations (-O0) and generating debug info (-g).

Running WAMR

You need to launch the runtime with the -g flag for debugging and specify an address that we can connect to with LLDB later. I’m using localhost (127.0.0.1) and port 4567. You may also need to increase the heap size.

./iwasm -g=127.0.0.1:4567 --heap-size=1048576 simple.wasm
[20:30:00:000 - 16D783000]: control thread of debug object 0x102f60aa0 start
[20:30:00:000 - 16D783000]: Debug server listening on 127.0.0.1:4567

This will launch the runtime and wait for the debugger to attach. Keep this running and switch to another terminal window.

Attaching from LLDB

Launch LLDB and connect to the Wasm runtime using the IP and port you specified earlier. Note that you have to explicitly specify the wasm plugin.

$ lldb
(lldb) process connect --plugin wasm connect://localhost:4567
Process 1 stopped
* thread #1, name = 'nobody', stop reason = trace
    frame #0: 0x40000000000001ad
simple.wasm`main:
->  0x40000000000001ad <+3>:  global.get 0
    0x40000000000001b3 <+9>:  i32.const 16
    0x40000000000001b5 <+11>: i32.sub
    0x40000000000001b6 <+12>: local.set 0

Now you can set a symbolic breakpoint on add and continue to the breakpoint.

(lldb) b add
Breakpoint 1: where = simple.wasm`add + 28 at simple.c:2:10, address = 0x400000000000019c
(lldb) c
Process 1 resuming
Process 1 stopped
* thread #1, name = 'nobody', stop reason = breakpoint 1.1
    frame #0: 0x400000000000019c simple.wasm`add(a=1, b=2) at simple.c:2:10
   1    int add(int a, int b) {
-> 2      return a + b;
   3    }
   4
   5    int main() {
   6      int i = 1;
   7      int j = 2;

You can print the backtrace with bt and show variables with frame var.

(lldb) bt
* thread #1, name = 'nobody', stop reason = breakpoint 1.1
  * frame #0: 0x400000000000019c simple.wasm`add(a=1, b=2) at simple.c:2:10
    frame #1: 0x40000000000001e5 simple.wasm`main at simple.c:8:10
    frame #2: 0x40000000000001fe simple.wasm
(lldb) frame var
(int) a = 1
(int) b = 2

Configuring the WebAssembly Platform

The workflow above requires launching the runtime by hand, picking a port, and connecting to it explicitly. Recent versions of LLDB have a WebAssembly platform that automates this. When configured, you can launch LLDB with a Wasm binary, and it’ll run it (using the configured runtime) just like it would a native binary.

The platform requires configuring a runtime. Here’s what that would look like for WAMR:

(lldb) settings set -- platform.plugin.wasm.runtime-path /path/to/iwasm
(lldb) settings set -- platform.plugin.wasm.port-arg -g=127.0.0.1:
(lldb) settings set -- platform.plugin.wasm.runtime-args --heap-size=1048576

Note the -- after settings set which is necessary to prevent LLDB from interpreting leading dashes as command options.

  • The port-arg is the flag the runtime uses to specify the debug port (-g=127.0.0.1:). LLDB will use the next free port and concatenate it.
  • The runtime-args carries anything else you want to pass to the runtime, such as the heap size.

LLDB will automatically select the Wasm platform when passed a binary with a Wasm triple.

$ lldb simple.wasm
(lldb) target create "simple.wasm"
Current executable set to 'simple.wasm' (wasm32).
(lldb) b add
Breakpoint 1: where = simple.wasm`add + 28 at simple.c:2:10, address = 0x400000000000019c
(lldb) r
Process 1 launched: 'simple.wasm' (wasm32)
Process 1 stopped
* thread #1, name = 'nobody', stop reason = breakpoint 1.1
    frame #0: 0x400000000000019c simple.wasm`add(a=1, b=2) at simple.c:2:10
   1    int add(int a, int b) {
-> 2      return a + b;
   3    }