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 checkout a specific release.

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

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=ON) 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=ON -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