From 94d0ee64ab317a2329cbbc27f60c625cf3ab47fb Mon Sep 17 00:00:00 2001 From: TianlongLiang Date: Thu, 25 Jun 2026 10:16:01 +0800 Subject: [PATCH] samples(debug-tools-optimized): add production-debug workflow sample New self-contained sample demonstrating the full production-debug workflow for optimized WASM: - 4-step build pipeline (clang -Oz -g -flto, wasm-opt -Oz -g, llvm-strip --strip-all, wamrc) producing prod.wasm + prod.aot + debug.wasm companion artifacts. - Two test apps (oob, stackoverflow) split into multi-file C sources to exercise cross-TU LTO inlining. - USE_FAST_INTERP CMake option to build iwasm in classic or fast-interp mode for testing. - symbolicate.sh: end-to-end driver that runs iwasm on the prod binary, captures the call stack, and resolves it via addr2line.py + the debug companion. Auto-detects classic vs fast-interp from the iwasm binary. - verify.sh: per-(app, mode) assertion that the symbolicated output contains the expected source files. Used by CI. Depends on the addr2line.py refactor (--mode flag is invoked from symbolicate.sh). --- samples/debug-tools-optimized/CMakeLists.txt | 89 ++++++ samples/debug-tools-optimized/README.md | 283 ++++++++++++++++++ samples/debug-tools-optimized/symbolicate.sh | 109 +++++++ samples/debug-tools-optimized/verify.sh | 108 +++++++ .../wasm-apps/CMakeLists.txt | 119 ++++++++ .../wasm-apps/oob_access.c | 12 + .../wasm-apps/oob_main.c | 26 ++ .../wasm-apps/stackoverflow_main.c | 16 + .../wasm-apps/stackoverflow_recurse.c | 25 ++ 9 files changed, 787 insertions(+) create mode 100644 samples/debug-tools-optimized/CMakeLists.txt create mode 100644 samples/debug-tools-optimized/README.md create mode 100755 samples/debug-tools-optimized/symbolicate.sh create mode 100755 samples/debug-tools-optimized/verify.sh create mode 100644 samples/debug-tools-optimized/wasm-apps/CMakeLists.txt create mode 100644 samples/debug-tools-optimized/wasm-apps/oob_access.c create mode 100644 samples/debug-tools-optimized/wasm-apps/oob_main.c create mode 100644 samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c create mode 100644 samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c diff --git a/samples/debug-tools-optimized/CMakeLists.txt b/samples/debug-tools-optimized/CMakeLists.txt new file mode 100644 index 0000000000..622b96f981 --- /dev/null +++ b/samples/debug-tools-optimized/CMakeLists.txt @@ -0,0 +1,89 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) + +include(CheckPIESupported) + +project(debug_tools_optimized_sample) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/../cmake) + +# WAMR features switch +string(TOLOWER ${CMAKE_HOST_SYSTEM_NAME} WAMR_BUILD_PLATFORM) +if (APPLE) + add_definitions(-DBH_PLATFORM_DARWIN) +endif () + +set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") +set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + +if (NOT DEFINED WAMR_BUILD_TARGET) + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)") + set (WAMR_BUILD_TARGET "AARCH64") + elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "riscv64") + set (WAMR_BUILD_TARGET "RISCV64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) + set (WAMR_BUILD_TARGET "X86_64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 4) + set (WAMR_BUILD_TARGET "X86_32") + else () + message(SEND_ERROR "Unsupported build target platform!") + endif () +endif () + +if (NOT CMAKE_BUILD_TYPE) + set (CMAKE_BUILD_TYPE Release) +endif () + +option(USE_FAST_INTERP "Build iwasm with fast interpreter instead of classic interpreter" OFF) + +set(WAMR_BUILD_INTERP 1) +set(WAMR_BUILD_LIBC_WASI 1) +if (USE_FAST_INTERP) + set(WAMR_BUILD_FAST_INTERP 1) + message(STATUS "Building iwasm with fast interpreter (USE_FAST_INTERP=ON)") +else () + set(WAMR_BUILD_FAST_INTERP 0) + message(STATUS "Building iwasm with classic interpreter (default; pass -DUSE_FAST_INTERP=ON for fast-interp)") +endif () +set(WAMR_BUILD_AOT 1) +set(WAMR_BUILD_DUMP_CALL_STACK 1) +# Disable hardware bound checks so OOB memory traps go through the +# interpreter's exception path (which calls SYNC_ALL_TO_FRAME and captures +# frame_ip), instead of the SIGSEGV signal-handler-and-longjmp path +# (which doesn't update frame->ip). Capturing frame_ip is what lets +# addr2line.py recover inline frames for the OOB sample. +set(WAMR_DISABLE_HW_BOUND_CHECK 1) + +if (NOT (CMAKE_C_COMPILER MATCHES ".*clang.*" OR CMAKE_C_COMPILER_ID MATCHES ".*Clang")) + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +endif () + +set(WAMR_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..) +include (${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake) + +add_library(vmlib ${WAMR_RUNTIME_LIB_SOURCE}) + +################ wasm application ################ +include(ExternalProject) + +ExternalProject_Add(wasm-apps + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/wasm-apps" + CONFIGURE_COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/wasm-apps -B build + BUILD_COMMAND ${CMAKE_COMMAND} --build build + INSTALL_COMMAND ${CMAKE_COMMAND} --install build --prefix ${CMAKE_CURRENT_BINARY_DIR} +) + +################ wamr runtime ################ +include (${SHARED_DIR}/utils/uncommon/shared_uncommon.cmake) + +set (RUNTIME_SOURCE_ALL + ${CMAKE_CURRENT_LIST_DIR}/../../product-mini/platforms/linux/main.c + ${UNCOMMON_SHARED_SOURCE} +) +add_executable (iwasm ${RUNTIME_SOURCE_ALL}) +check_pie_supported() +set_target_properties (iwasm PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_link_libraries(iwasm vmlib -lm -ldl) +add_dependencies(iwasm wasm-apps) diff --git a/samples/debug-tools-optimized/README.md b/samples/debug-tools-optimized/README.md new file mode 100644 index 0000000000..ac9c2814e9 --- /dev/null +++ b/samples/debug-tools-optimized/README.md @@ -0,0 +1,283 @@ +# debug-tools-optimized — Debugging Production-Optimized WASM + +This sample demonstrates symbolication of crashes in **production-optimized** WASM +binaries using the merged `addr2line.py`. The wasm apps are compiled with +`-Oz -g -flto` and post-processed with `wasm-opt -Oz -g`, then stripped to produce +minimal production binaries. A "debug companion" binary built in parallel retains +DWARF inline info, enabling source-level call stack recovery offline. + +## Why this exists alongside `samples/debug-tools/` + +The existing `samples/debug-tools/` sample uses `-O0 -g`: each function is preserved, +no inlining happens. This sample uses `-Oz -g -flto`, which: + +- Aggressively inlines functions across translation units (cross-TU inlining via LTO) +- Strips the production binary to minimum size +- Tests `addr2line.py`'s inline expansion (`DW_TAG_inlined_subroutine` resolution) + +If you only need to debug development builds, `samples/debug-tools/` is sufficient. +If you need to debug **shipped** binaries, this sample shows how. + +## Build pipeline + +``` +clang -Oz -g -flto source1.c source2.c → .wasm (intermediate) + └─ wasm-opt -Oz -g → .debug.wasm (companion: code + DWARF + names) + └─ llvm-strip --strip-all → .prod.wasm (production: code only) +``` + +## Why production is derived from the debug companion + +`wasm-opt -Oz` (without `-g`) and `wasm-opt -Oz -g` produce **structurally different +binaries**: `-g` inhibits some inlining passes to preserve DWARF integrity. If we ran +them as separate pipelines, the production binary's code offsets would not match the +companion's DWARF address space — offline decode would silently break. + +Instead, we run `wasm-opt -Oz -g` once and derive production by stripping the +companion (`llvm-strip --strip-all`). Custom sections (DWARF, names) live *after* the +code section in the WASM binary format, so stripping them doesn't shift code offsets. +This **guarantees byte-identical code** between production and companion. + +## Why `-flto` + +Without LTO, functions in separate `.c` files remain separate WASM functions even +under `-Oz`. With LTO, the compiler sees all sources as one unit and inlines +aggressively across files. This is what makes the `do_bad_access → trigger_oob → +app_main` chain collapse into a single WASM function with multiple inlined +subroutines. + +## Why `recurse()` is non-tail-recursive + +`stackoverflow_recurse.c` uses +`int r = recurse(depth + 1); return r + buf[0];` instead of +`return recurse(depth + 1);`. Tail calls (`return f(...)`) get converted to loops at +`-Oz -flto`, eliminating the recursion that we want to test. The non-tail form forces +a real `call` instruction so each iteration pushes a new frame. + +## Prerequisites + +- wasi-sdk at `WASI_SDK_PATH` or `/opt/wasi-sdk` +- binaryen at `BINARYEN_PATH` or `/opt/binaryen` +- wabt at `WABT_PATH` or `/opt/wabt` +- Python 3 + +## Quick start + +```bash +mkdir -p build && cd build +cmake .. && make -j$(nproc) +cd .. + +# wasm (interpreter) +./symbolicate.sh oob +./symbolicate.sh stackoverflow + +# aot +./symbolicate.sh oob aot +./symbolicate.sh stackoverflow aot +``` + +To build with the fast interpreter instead of the classic interpreter: + +```bash +mkdir -p build && cd build +cmake .. -DUSE_FAST_INTERP=ON && make -j$(nproc) +``` + +The same `symbolicate.sh` invocations work for both build modes — it auto-detects +which interpreter the iwasm binary uses (by inspecting it for the `wasm_interp_fast.c` +symbol) and passes the right `--mode` to `addr2line.py`. + +### `verify.sh` — assertion-based smoke test + +`verify.sh ` runs `symbolicate.sh` and asserts the +symbolicated output matches the expected shape: for the OOB sample, +all three inline frames (`do_bad_access (inlined into trigger_oob)`, +`trigger_oob (inlined into app_main)`, `app_main`) plus their source +files; for the stackoverflow sample, `recurse` and `app_main` resolved +across the recursive chain. It auto-detects classic vs fast-interp +builds and relaxes the OOB assertion for fast-interp + wasm (where the +runtime offset doesn't map to source, so addr2line.py only emits the +outermost function name). Used by CI; useful locally to check a build: + +```bash +./verify.sh oob wasm # PASS or fails with the captured output +./verify.sh stackoverflow aot +``` + +The build pipeline produces three artifacts per app: + +| Artifact | Purpose | +|----------|---------| +| `.debug.wasm` | Debug companion — DWARF + name section, used for symbolication | +| `.prod.wasm` | Production wasm — fully stripped, runs in interpreter mode | +| `.prod.aot` | Production AOT — wamrc-compiled with `--enable-dump-call-stack` | + +Both `.prod.wasm` and `.prod.aot` use the same `.debug.wasm` companion for offline +symbolication. AOT and classic-interp produce nearly identical call stack offsets +(within 1-2 bytes), and `addr2line.py` resolves both correctly. + +## Build-side tools (and why each is needed) + +This sample chains four compile-time and post-build tools, each filling a +specific gap; dropping any of them would break the inline-DWARF round trip +the sample is meant to demonstrate: + +| Tool | From | What it does here | Why we need it | +|------|------|-------------------|----------------| +| `clang -Oz -g -flto` | wasi-sdk | Compiles `.c` → `.wasm` with size-optimized code, DWARF debug info, and link-time optimization | `-flto` enables cross-translation-unit inlining. Without LTO, `oob_main.c` and `oob_access.c` would stay as separate WASM functions; with LTO they collapse into a single function with `DW_TAG_inlined_subroutine` entries — exactly what we need to test inline-aware symbolication. | +| `wasm-opt -Oz -g` | binaryen | Post-optimization pass on the wasm; preserves DWARF integrity | Further size shrink (LEB compaction, dead-code elimination) on top of clang. The `-g` flag inhibits transformations that would corrupt DWARF, producing a "debug companion" with the same code layout as production. | +| `llvm-strip --strip-all` | wasi-sdk | Strips DWARF and name sections from the companion → production binary | Custom sections (DWARF, name) live AFTER the code section in the wasm format, so stripping them doesn't shift code offsets. This guarantees the production binary's runtime offsets map 1:1 to the companion's DWARF — without this property, offline decode would silently break. | +| `wamrc --enable-dump-call-stack --bounds-checks=1` | wamr-compiler | Compiles `.wasm` → `.aot` for ahead-of-time execution | AOT is the realistic embedded deployment target. `--bounds-checks=1` forces software memory bounds checks instead of hardware traps, so OOB exceptions go through the runtime exception path (which captures `frame_ip`) instead of a SIGSEGV that bypasses ip capture — without this, the OOB sample's AOT path would crash with no captured call stack. | + +For the **decode-side** tools (`llvm-symbolizer`, `llvm-dwarfdump`, +`wasm-objdump`, `llvm-cxxfilt`) and the interval-table overlay that +addr2line.py applies to correct the LLVM symbolizer's outermost +function-name lookup on wasm, see +[`test-tools/addr2line/README.md`](../../test-tools/addr2line/README.md). + +## Three execution modes, three offset spaces + +`addr2line.py` supports a `--mode={interp,aot,fast-interp}` flag because each +mode reports offsets differently: + +| Mode | Offset space | Adjustment | Source resolution | +|------|--------------|-----------|-------------------| +| `interp` (default) | File-absolute, post-advance | `offset - code_start - 1` | Full file:line, inline expansion | +| `aot` | File-absolute, instruction-start | `offset - code_start` | Full file:line, inline expansion | +| `fast-interp` | Function-relative, transformed bytecode | (not mappable) | Function-name only | + +**Why fast-interp can't show source lines**: at load time, fast-interp rewrites the +WASM bytecode in memory (replaces opcodes with handler indices, expands LEBs to +fixed widths, etc.). The runtime ip then points into this *transformed* buffer, not +the original WASM bytes — so there's no way to map the offset back to a source line. +Function-name lookup still works via `wasm-objdump` + `llvm-dwarfdump --name=...`. + +`symbolicate.sh` handles all three modes transparently. For manual invocation: + +```bash +# Classic interp (default) +python3 ../../test-tools/addr2line/addr2line.py --wasm-file ... callstack.txt + +# AOT +python3 ../../test-tools/addr2line/addr2line.py --mode=aot --wasm-file ... callstack.txt + +# Fast interp +python3 ../../test-tools/addr2line/addr2line.py --mode=fast-interp --wasm-file ... callstack.txt +``` + +## Why `iwasm -f app_main` (and not just `iwasm `) + +The `symbolicate.sh` script invokes `iwasm -f app_main` instead of letting iwasm run +the default wasi `_start` entry. This matters for two reasons: + +1. **Compiler folding**: Under `-Oz -flto`, when control reaches the OOB write through + `_start → __wasi_main_void → main → app_main → ...`, LLVM observes the entire chain + leading to undefined behavior and may rewrite it as `unreachable`. Calling + `app_main` directly preserves the explicit OOB instruction and produces the + expected `out of bounds memory access` exception. + +2. **Cleaner trap point**: With `-f app_main`, the WAMR call stack starts at our app's + entry, not deep inside wasi-libc startup, making the symbolication output more + focused on user code. + +## Expected output + +### oob app + +``` +=== Running iwasm on oob.prod.wasm (expect crash) === + +#00: 0x1dd8 - app_main + +Exception: out of bounds memory access + +=== Captured call stack === +#00: 0x1dd8 - app_main + +=== Symbolicated call stack (using debug companion) === +0: do_bad_access (inlined into trigger_oob) + at .../wasm-apps/oob_access.c:11:15 + trigger_oob (inlined into app_main) + at .../wasm-apps/oob_main.c:17:5 + app_main + at .../wasm-apps/oob_main.c:23:5 +``` + +Although `do_bad_access` and `trigger_oob` were both inlined into `app_main` +under `-Oz -flto` (the runtime sees only a single WASM function), the debug +companion's DWARF retains `DW_TAG_inlined_subroutine` entries that describe +the inline chain. `addr2line.py` walks them and reports the trap site at all +three source levels. + +This works because the iwasm in this sample is built with +`WAMR_DISABLE_HW_BOUND_CHECK=1`: OOB memory access goes through the +interpreter's exception path (which captures `frame_ip` via +`SYNC_ALL_TO_FRAME`), not through a SIGSEGV signal handler that +longjmps out without updating ip. The AOT build uses `--bounds-checks=1` +for the same reason. + +### stackoverflow app + +``` +=== Running iwasm on stackoverflow.prod.wasm (expect crash) === + +#00: 0x1e15 - $f12 +#01: 0x1e15 - $f12 + ... (~20 identical frames as the wasm operand stack overflows) ... +#21: 0x1e15 - $f12 +#22: 0x1dd0 - app_main + +Exception: wasm operand stack overflow + +=== Captured call stack === +... same as above ... + +=== Symbolicated call stack (using debug companion) === +0: recurse + at .../wasm-apps/stackoverflow_recurse.c:20:13 + ... (one resolved frame per captured frame) ... +22: app_main + at .../wasm-apps/stackoverflow_main.c:14:5 +``` + +Stack overflow produces non-zero offsets at every frame (the runtime +captures the ip of the call instruction in each caller), so `addr2line.py` +resolves source file, line number, and function name correctly across the +recursive chain. `llvm-symbolizer`'s outermost frame's function name is +unreliable on wasm for two independent reasons: LLVM unconditionally +overrides that frame's name from an object-file symbol-table lookup +(which can return a wrong function on some wasm layouts), and +`wasm-opt -Oz -g` leaves dead-code-eliminated `DW_TAG_subprogram` DIEs +behind with `low_pc = 0` which then violate DWARF's DIE-range map +invariants. `addr2line.py` always applies an interval-table overlay +to the outermost frame's name, so it renders as `recurse` / `app_main` +in both cases. + +## Manual decode + +If you have a captured stack from another iwasm run (e.g., from a remote board or +saved log), you can symbolicate it directly: + +```bash +python3 ../../test-tools/addr2line/addr2line.py \ + --wasi-sdk /opt/wasi-sdk \ + --wabt /opt/wabt \ + --wasm-file build/wasm-apps/oob.debug.wasm \ + /path/to/saved/call_stack.txt +``` + +## Environment variables + +| Variable | Default | Used by | +|----------|---------|---------| +| `WASI_SDK_PATH` | `/opt/wasi-sdk` | Build (clang, llvm-strip) and decode (llvm-symbolizer, llvm-dwarfdump) | +| `BINARYEN_PATH` | `/opt/binaryen` | Build (wasm-opt) | +| `WABT_PATH` | `/opt/wabt` | Decode (wasm-objdump) | + +## References + +- [addr2line.py](../../test-tools/addr2line/addr2line.py) +- [WAMR Dump Call Stack Feature](../../doc/build_wamr.md#dump-call-stack-feature) +- [Zephyr coredump-debug sample](../../product-mini/platforms/zephyr/coredump-debug/) — same workflow on embedded +- [debug-tools sample](../debug-tools/) — non-optimized debug build for comparison diff --git a/samples/debug-tools-optimized/symbolicate.sh b/samples/debug-tools-optimized/symbolicate.sh new file mode 100755 index 0000000000..1b77fc897b --- /dev/null +++ b/samples/debug-tools-optimized/symbolicate.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +set -euo pipefail + +# Run a wasm or aot app, capture the WAMR call stack, and symbolicate it +# using addr2line.py with the debug companion. +# +# Usage: ./symbolicate.sh [oob|stackoverflow] [wasm|aot] + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +APP="${1:-oob}" +MODE="${2:-wasm}" + +if [ "$APP" != "oob" ] && [ "$APP" != "stackoverflow" ]; then + echo "Usage: $0 [oob|stackoverflow] [wasm|aot]" >&2 + exit 1 +fi +if [ "$MODE" != "wasm" ] && [ "$MODE" != "aot" ]; then + echo "Usage: $0 [oob|stackoverflow] [wasm|aot]" >&2 + exit 1 +fi + +WASI_SDK_PATH="${WASI_SDK_PATH:-/opt/wasi-sdk}" +WABT_PATH="${WABT_PATH:-/opt/wabt}" +WAMR_ROOT="${SCRIPT_DIR}/../.." + +BUILD_DIR="${SCRIPT_DIR}/build" +if [ "$MODE" = "wasm" ]; then + PROD_FILE="${BUILD_DIR}/wasm-apps/${APP}.prod.wasm" +else + PROD_FILE="${BUILD_DIR}/wasm-apps/${APP}.prod.aot" +fi +DEBUG_WASM="${BUILD_DIR}/wasm-apps/${APP}.debug.wasm" +IWASM="${BUILD_DIR}/iwasm" + +if [ ! -x "${IWASM}" ]; then + echo "iwasm not found at ${IWASM}" >&2 + echo "Run: mkdir -p build && cd build && cmake .. && make" >&2 + exit 1 +fi +if [ ! -f "${PROD_FILE}" ]; then + echo "Production binary not found at ${PROD_FILE}" >&2 + exit 1 +fi +if [ ! -f "${DEBUG_WASM}" ]; then + echo "Debug companion not found at ${DEBUG_WASM}" >&2 + exit 1 +fi + +CALL_STACK_FILE=$(mktemp) +LOG_FILE=$(mktemp) +trap 'rm -f "${CALL_STACK_FILE}" "${LOG_FILE}"' EXIT + +echo "=== Running iwasm on ${APP}.prod.${MODE} (expect crash) ===" +# -f app_main calls the exported app_main directly, bypassing wasi _start. +# This preserves the OOB / stack-overflow trap behavior — running _start +# under -Oz -flto would lower the OOB pattern to `unreachable` and produce +# misleading call-stack info. +# +# stackoverflow uses --stack-size=4096 so the WASM operand stack overflows +# at a reasonable depth (~20 frames). With the default stack size the +# recursion would still trap, just much later (~400 frames) and with a +# much larger captured call-stack to symbolicate. +IWASM_ARGS=() +if [ "$APP" = "stackoverflow" ]; then + IWASM_ARGS+=(--stack-size=4096) +fi +"${IWASM}" "${IWASM_ARGS[@]}" -f app_main "${PROD_FILE}" 2>&1 | tee "${LOG_FILE}" || true + +echo "" +echo "=== Captured call stack ===" +grep -E "^#[0-9]+:" "${LOG_FILE}" > "${CALL_STACK_FILE}" || true +cat "${CALL_STACK_FILE}" + +if [ ! -s "${CALL_STACK_FILE}" ]; then + echo "(no call stack captured)" + exit 1 +fi + +echo "" +echo "=== Symbolicated call stack (using debug companion) ===" +# Pick the right --mode for addr2line.py: +# - aot: offsets are file-absolute, no adjustment (wamrc commits ip +# at instruction start). Always use --mode=aot regardless of +# how iwasm itself was built. +# - wasm + classic interp: offsets are file-absolute, post-advance → --mode=interp +# - wasm + fast interp: offsets are function-relative in transformed +# bytecode → --mode=fast-interp (function-name lookup only) +# +# Detect fast-interp by inspecting CMakeCache.txt for WAMR_BUILD_FAST_INTERP=1. +if [ "$MODE" = "aot" ]; then + A2L_MODE=aot +else + A2L_MODE=interp + # Detect fast-interp by inspecting iwasm for the wasm_interp_fast.c symbol. + if strings "${IWASM}" 2>/dev/null | grep -q "wasm_interp_fast.c"; then + A2L_MODE=fast-interp + fi +fi + +# DWARF only lives in the .debug.wasm — addr2line.py uses it for all modes. +python3 "${WAMR_ROOT}/test-tools/addr2line/addr2line.py" \ + --wasi-sdk "${WASI_SDK_PATH}" \ + --wabt "${WABT_PATH}" \ + --wasm-file "${DEBUG_WASM}" \ + --mode "${A2L_MODE}" \ + "${CALL_STACK_FILE}" diff --git a/samples/debug-tools-optimized/verify.sh b/samples/debug-tools-optimized/verify.sh new file mode 100755 index 0000000000..e77b14cede --- /dev/null +++ b/samples/debug-tools-optimized/verify.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Verify symbolicated output for one (app, mode) combination. +# +# Usage: ./verify.sh +# +# Runs ./symbolicate.sh and asserts the output contains the expected source +# file references. Auto-detects whether iwasm was built with classic or fast +# interpreter (via the symbolicate.sh script itself) and relaxes the +# fast-interp + oob + wasm assertion since fast-interp can't resolve +# offset=0 (trap-at-entry) to a source line. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +APP="${1:-}" +MODE="${2:-}" + +if [ "$APP" != "oob" ] && [ "$APP" != "stackoverflow" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi +if [ "$MODE" != "wasm" ] && [ "$MODE" != "aot" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +# Detect interpreter mode by checking the iwasm binary (same logic as symbolicate.sh) +IWASM="${SCRIPT_DIR}/build/iwasm" +if [ ! -x "$IWASM" ]; then + echo "iwasm not found at $IWASM; build the sample first" >&2 + exit 1 +fi +INTERP="classic" +if strings "$IWASM" 2>/dev/null | grep -q "wasm_interp_fast.c"; then + INTERP="fast" +fi + +OUT=$(mktemp) +trap 'rm -f "$OUT"' EXIT + +"${SCRIPT_DIR}/symbolicate.sh" "$APP" "$MODE" 2>&1 | tee "$OUT" > /dev/null + +assert() { + local pattern="$1" + if ! grep -q "$pattern" "$OUT"; then + echo "FAIL [$INTERP/$APP/$MODE]: pattern '$pattern' not found in output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +# assert_re: assert a POSIX extended regex matches somewhere in the captured output. +# Used for compound expectations like "function name X on its own line". +assert_re() { + local pattern="$1" + if ! grep -Eq "$pattern" "$OUT"; then + echo "FAIL [$INTERP/$APP/$MODE]: regex '$pattern' did not match output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +case "$APP" in + oob) + # Runtime always reports the OOB exception type. + assert "out of bounds memory access" + # do_bad_access -> trigger_oob -> app_main are all inlined into a + # single WASM function under -Oz -flto. On classic-interp + wasm + # and aot builds, DWARF retains the inline chain, so addr2line.py + # emits all three names with "(inlined into )" annotations. + # On fast-interp + wasm the runtime offset doesn't map to source + # (transformed bytecode), so addr2line.py falls back to + # function-name lookup and emits only the outermost frame. + if [ "$INTERP" = "fast" ] && [ "$MODE" = "wasm" ]; then + assert_re '^0: app_main$' + else + assert_re '^0: do_bad_access \(inlined into trigger_oob\)$' + assert_re '^[[:space:]]+trigger_oob \(inlined into app_main\)$' + assert_re '^[[:space:]]+app_main$' + assert "oob_access.c" + assert "oob_main.c" + fi + ;; + stackoverflow) + # stackoverflow_recurse.c uses non-tail recursion that reliably + # exhausts the WASM operand stack (with --stack-size=4096 in + # symbolicate.sh, depth lands around 20 frames). + assert "wasm operand stack overflow" + # The captured call stack is recurse repeated, ending in app_main. + # addr2line.py must resolve both function names exactly. + assert_re '^[0-9]+: recurse$' + assert "stackoverflow_recurse.c" + if [ "$INTERP" = "fast" ] && [ "$MODE" = "wasm" ]; then + # fast-interp falls back to function-name lookup; the + # outermost app_main frame may not surface stackoverflow_main.c + # but the function name itself does. + assert_re '^[0-9]+: app_main$' + else + assert_re '^[0-9]+: app_main$' + assert "stackoverflow_main.c" + fi + ;; +esac + +echo "PASS [$INTERP/$APP/$MODE]" diff --git a/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt b/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt new file mode 100644 index 0000000000..b2a56b0f6d --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt @@ -0,0 +1,119 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) +project(debug_tools_optimized_wasm_apps LANGUAGES NONE) + +set(WAMR_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../..") + +# --- wasi-sdk tool discovery --- +if(NOT DEFINED WASI_SDK_PATH) + if(DEFINED ENV{WASI_SDK_PATH}) + set(WASI_SDK_PATH "$ENV{WASI_SDK_PATH}") + else() + set(WASI_SDK_PATH "/opt/wasi-sdk") + endif() +endif() + +set(WASI_SDK_CLANG "${WASI_SDK_PATH}/bin/clang") +set(WASI_SDK_STRIP "${WASI_SDK_PATH}/bin/llvm-strip") + +if(NOT EXISTS "${WASI_SDK_CLANG}") + message(FATAL_ERROR "wasi-sdk clang not found at ${WASI_SDK_CLANG}.") +endif() +if(NOT EXISTS "${WASI_SDK_STRIP}") + message(FATAL_ERROR "llvm-strip not found at ${WASI_SDK_STRIP}.") +endif() + +# --- binaryen (wasm-opt) discovery --- +if(NOT DEFINED BINARYEN_PATH) + if(DEFINED ENV{BINARYEN_PATH}) + set(BINARYEN_PATH "$ENV{BINARYEN_PATH}") + else() + set(BINARYEN_PATH "/opt/binaryen") + endif() +endif() + +set(WASM_OPT "${BINARYEN_PATH}/bin/wasm-opt") +if(NOT EXISTS "${WASM_OPT}") + message(FATAL_ERROR "wasm-opt not found at ${WASM_OPT}. Set BINARYEN_PATH.") +endif() + +# --- wamrc discovery (for AOT) --- +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake) +find_package(WAMRC REQUIRED) + +set(WASM_COMPILE_FLAGS + --target=wasm32-wasi + -Oz -g -flto + -Wl,--export=app_main +) + +# Build a wasm app from one or more source files. +# Usage: build_wasm_app( [ ...]) +function(build_wasm_app APP_NAME) + set(SOURCES ${ARGN}) + + set(SOURCE_PATHS "") + foreach(SRC ${SOURCES}) + list(APPEND SOURCE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/${SRC}") + endforeach() + + set(WASM_INTERMEDIATE "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.wasm") + set(WASM_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.debug.wasm") + set(WASM_PROD "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.prod.wasm") + set(AOT_PROD "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.prod.aot") + + # 1. Compile all sources together with -Oz -g -flto + add_custom_command( + OUTPUT "${WASM_INTERMEDIATE}" + COMMAND ${WASI_SDK_CLANG} ${WASM_COMPILE_FLAGS} + -o "${WASM_INTERMEDIATE}" + ${SOURCE_PATHS} + DEPENDS ${SOURCE_PATHS} + COMMENT "[${APP_NAME}] Compiling -> ${APP_NAME}.wasm (intermediate, -Oz -g -flto)" + ) + + # 2. Debug companion (preserves DWARF + name section) + add_custom_command( + OUTPUT "${WASM_DEBUG}" + COMMAND ${WASM_OPT} -Oz -g -o "${WASM_DEBUG}" "${WASM_INTERMEDIATE}" + DEPENDS "${WASM_INTERMEDIATE}" + COMMENT "[${APP_NAME}] wasm-opt -Oz -g -> ${APP_NAME}.debug.wasm (companion)" + ) + + # 3. Production (strip debug companion to guarantee identical code) + add_custom_command( + OUTPUT "${WASM_PROD}" + COMMAND ${WASI_SDK_STRIP} --strip-all -o "${WASM_PROD}" "${WASM_DEBUG}" + DEPENDS "${WASM_DEBUG}" + COMMENT "[${APP_NAME}] strip -> ${APP_NAME}.prod.wasm (production)" + ) + + # 4. AOT (compiled from production wasm with --enable-dump-call-stack) + # The .debug.wasm companion is still used for symbolication — DWARF only + # lives in the wasm; AOT carries the call-stack metadata only. + # --bounds-checks=1 forces explicit memory bounds checks instead of + # relying on hardware traps; this matches the iwasm build (which + # disables HW bound checks so OOB traps go through the interpreter + # exception path and capture frame_ip — without it, the AOT path + # would SIGSEGV with no captured call stack and inline DWARF info + # would be unrecoverable on the OOB sample. + add_custom_command( + OUTPUT "${AOT_PROD}" + DEPENDS ${WAMRC_BIN} "${WASM_PROD}" + COMMAND ${WAMRC_BIN} --size-level=0 --enable-dump-call-stack + --bounds-checks=1 + -o "${AOT_PROD}" "${WASM_PROD}" + COMMENT "[${APP_NAME}] wamrc -> ${APP_NAME}.prod.aot (AOT)" + ) + + add_custom_target(${APP_NAME}_wasm ALL + DEPENDS "${WASM_INTERMEDIATE}" "${WASM_DEBUG}" "${WASM_PROD}" "${AOT_PROD}" + ) + + install(FILES "${WASM_DEBUG}" "${WASM_PROD}" "${AOT_PROD}" DESTINATION wasm-apps) +endfunction() + +build_wasm_app(oob oob_main.c oob_access.c) +build_wasm_app(stackoverflow stackoverflow_main.c stackoverflow_recurse.c) diff --git a/samples/debug-tools-optimized/wasm-apps/oob_access.c b/samples/debug-tools-optimized/wasm-apps/oob_access.c new file mode 100644 index 0000000000..ccb08a2943 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/oob_access.c @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +void +do_bad_access(int offset) +{ + volatile int *p = (volatile int *)0; + /* Write to an address way beyond linear memory to trigger OOB trap */ + p[offset] = 0xDEAD; +} diff --git a/samples/debug-tools-optimized/wasm-apps/oob_main.c b/samples/debug-tools-optimized/wasm-apps/oob_main.c new file mode 100644 index 0000000000..30a8b5e948 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/oob_main.c @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Deliberate out-of-bounds memory access for coredump debug demo. + Multi-file: app_main -> trigger_oob (this file) -> do_bad_access + (oob_access.c) Under -Oz, these get inlined into app_main. The debug + companion retains DWARF inline info so the full chain can be recovered + offline. */ + +void +do_bad_access(int offset); + +void +trigger_oob(void) +{ + /* 0x7FFFFFFF is well beyond any WASM linear memory */ + do_bad_access(0x7FFFFFFF); +} + +void +app_main(void) +{ + trigger_oob(); +} diff --git a/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c b/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c new file mode 100644 index 0000000000..0e57a2e640 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Recursive stack overflow for coredump debug demo. + Multi-file: app_main (this file) -> recurse (stackoverflow_recurse.c) */ + +int +recurse(int depth); + +void +app_main(void) +{ + recurse(0); +} diff --git a/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c b/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c new file mode 100644 index 0000000000..0cbf7a37bf --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +static volatile int sink; + +int +recurse(int depth) +{ + /* Allocate stack space each frame to accelerate overflow */ + volatile char buf[128]; + buf[0] = (char)depth; + buf[127] = (char)(depth >> 8); + sink = buf[0] + buf[127]; + if (depth == 10000) { + __builtin_trap(); + } + /* TODO: Use return value after the call to prevent tail-call optimization, + * it will stack overflow */ + int r = recurse(depth + 1); + return r + buf[0]; + /* TODO: will optimize to loop, and it will trap when reach certain level */ + // return recurse(depth + 1); +}