Skip to content

addr2line: enhance call-stack symbolicator, samples, tests, and CI#4972

Open
TianlongLiang wants to merge 1 commit into
bytecodealliance:mainfrom
TianlongLiang:dev/addr2line_enhance
Open

addr2line: enhance call-stack symbolicator, samples, tests, and CI#4972
TianlongLiang wants to merge 1 commit into
bytecodealliance:mainfrom
TianlongLiang:dev/addr2line_enhance

Conversation

@TianlongLiang

Copy link
Copy Markdown
Contributor

Refactor test-tools/addr2line/addr2line.py to reliably symbolicate WAMR call stacks across the wasi-sdk versions WAMR supports (29+), and add test infrastructure plus two end-to-end debug-tools samples that exercise the workflow.

addr2line.py:

  • Detect wasi-sdk clang version at startup and dispatch to a "modern" resolver (clang >= 22, single llvm-symbolizer call) or a "legacy" resolver (clang < 22, with a DW_TAG_subprogram interval-table overlay built from a single llvm-dwarfdump --debug-info pass at startup). The overlay fixes the clang-22 wasm DWARF bug where the symbolizer returns the wrong outermost function name (e.g. recurse reported as free) for addresses overlapping wasi-libc declarations.
  • Inline frames are rendered with explicit "(inlined into )" annotations so the call chain reads top-down.
  • --no-addr (function-name lookup) prefers non-sysroot candidates so short user-code names (a, b, c) don't collide with wasi-libc helpers in the DWARF.
  • -v/--verbose gates the dispatch-decision log; default silent.
  • Require llvm-symbolizer; drop the legacy llvm-addr2line fallback (every supported wasi-sdk ships symbolizer).

samples/debug-tools (refresh):

  • Use always_inline trap_helper to demonstrate inline expansion.
  • symbolicate.sh is self-locating and prints banner headers per mode (interp / --no-addr / aot) so the three output blocks are distinguishable.
  • verify.sh asserts the precise frame names and inline annotation using anchored regex.
  • Fix make -j race in wasm-apps/CMakeLists.txt (explicit add_dependencies for the AOT custom target).

samples/debug-tools-optimized (new):

  • Production-realistic workflow: clang -Oz -g -flto then wasm-opt -Oz -g to produce a stripped .prod.wasm plus a DWARF companion (.debug.wasm) with byte-identical code.
  • AOT path via wamrc --enable-dump-call-stack --bounds-checks=1.
  • WAMR_DISABLE_HW_BOUND_CHECK=1 so OOB traps go through the interpreter exception path (capturing frame_ip) instead of the SIGSEGV signal handler (which doesn't update frame->ip).
  • Two apps: oob (cross-TU inlined memory trap) and stackoverflow (non-tail recursion that exhausts the operand stack at ~20 frames with --stack-size=4096).
  • verify.sh covers all (app, mode, interp) combinations.

test-tools/addr2line/tests (new):

  • Pytest suite covering 13 scenarios: simple resolution, basic and deep inline chains, cross-TU LTO inlining, mid-function trap, multi-frame call stack, C++ demangling, AOT mode, fast-interp fallback, --no-addr, offset=0 fallback, empty input, version dispatch, and modern-vs-legacy equivalence under --multi-sdk.
  • conftest.py exposes wasi_sdk/wabt/binaryen/build_wasm fixtures; filters SDK candidates by llvm-symbolizer presence; prints invocation IO under pytest -v -s.
  • 8 purpose-built C/C++ test apps under apps/, 3 fixture call stacks.

CI:

  • Per-PR job: addr2line_tests removed; debug-tools-optimized folded into build_samples_others (no separate runner / runtime install). Matrix-style classic+fast interp coverage via in-step loop.
  • Nightly: addr2line_tests_multi_sdk parametrizes pytest over wasi-sdk 29 and 33 for the modern/legacy equivalence test.
  • macOS workflow: existing debug-tools step now calls verify.sh.
  • install-wasi-sdk-wabt action: bump default wasi-sdk 25 -> 29 (pre-29 bundles lack llvm-symbolizer). New wasi_sdk_version input lets callers pin a version when needed.

Documentation:

  • New test-tools/addr2line/README.md explains the script's architecture (modern/legacy split, the LLVM symbolizer wasm bug, --mode={interp,aot,fast-interp} offset spaces, the four tools involved). Sample READMEs link there to avoid duplication.
  • tests/README.md describes every test case with the regression class it guards against.

@TianlongLiang

Copy link
Copy Markdown
Contributor Author

This is all in one PR; I will break it into a series of PRs so it is easier to review

…nd CI

Refactor test-tools/addr2line/addr2line.py to make its DWARF workaround
explicit and unconditional, add two verify.sh-backed samples that
exercise it end-to-end, add a pytest suite, and wire the whole thing
into CI.

## addr2line.py

Every resolution runs one `llvm-symbolizer -f -i` call per address,
then overlays the outermost frame's function name from a startup-built
DWARF interval table. The overlay exists because llvm-symbolizer returns
the wrong outermost function name on wasm targets for two independent,
source-verified reasons:

1. Symbol-table override on the outermost frame (LLVM design, not a
   bug). `symbolizeInlinedCode` in
   `llvm/lib/DebugInfo/Symbolize/SymbolizableObjectFile.cpp`
   unconditionally rewrites the outermost frame's `FunctionName` from
   `getNameFromSymbolTable` when `FunctionNameKind::LinkageName` and
   `UseSymbolTable` are both set — which is the default. Inline frames
   come from `getInliningInfoForAddress` (a different code path) and
   are provably immune.

2. `AddrDieMap` invariant violation from `wasm-opt -Oz -g` DCE ghosts.
   `LocationUpdater::getNewFuncStart` in binaryen's
   `src/wasm/wasm-debug.cpp` returns 0 as a tombstone when an old
   address doesn't map to a surviving IR node, and `updateDIE` writes
   it into `DW_AT_low_pc` without removing the `DW_TAG_subprogram`.
   LLVM's `updateAddressDieMap` in `DWARFUnit.cpp` requires child
   ranges ⊆ parent ranges; these ghosts break that invariant so the
   map's `upper_bound − 1` lookup returns an insertion-order-dependent
   DIE.

Both mechanisms produce the same "wrong outermost frame name" symptom
and both apply on any supported wasi-sdk, so the overlay runs on every
resolution. `build_subprogram_intervals` filters three DIE classes at
parse time: `DW_AT_declaration=true` (forward decls), `low_pc == 0`
(DWARF Code-section-relative zero is the function-count LEB, not any
real function body — the DCE-ghost pattern), and `high_pc <= low_pc`
(degenerate ranges). The innermost covering `DW_TAG_subprogram` wins.

Also includes accumulated tool improvements:
- --mode flag {interp,aot,fast-interp} for different runtime offset
  conventions (interp post-advance, aot at-instruction-start,
  fast-interp's transformed in-memory bytecode)
- Inline frame annotation "(inlined into <next>)" for clarity
- llvm-symbolizer preferred over llvm-addr2line for column info
- Fallback for offset=0 (trap at function entry; frame_ip not captured)
- Last-resort function-index name fallback when DWARF lacks PC ranges

Full rationale, source citations, and empirical truth table in
test-tools/addr2line/README.md.

## samples/debug-tools (refresh)

- README documents the --mode flag, inline expansion behavior, and
  points at the tool README for the overlay rationale.
- symbolicate.sh passes --mode aot for the AOT call-stack invocation.
- verify.sh (new): assertion-based smoke test that runs symbolicate.sh
  and grep-checks for inline expansion (trap_helper) in both wasm and
  AOT outputs. Replaces the inline CI assertions.
- wasm-apps/trap.c adds a trap_helper marked always_inline that the
  trap site falls inside, so addr2line.py can demonstrate inline-frame
  annotation against this sample.

## samples/debug-tools-optimized (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 → 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.
- symbolicate.sh: end-to-end driver.
- verify.sh: per-(app, mode) assertion that the symbolicated output
  contains the expected source files.

## test-tools/addr2line/tests (new)

pytest suite at test-tools/addr2line/tests/ exercising addr2line.py
against purpose-built C/C++ sources covering:
- Baseline single-function resolution
- Inline expansion (always_inline, 4-level chain)
- Cross-TU LTO inlining (multi-file recursion + wasm-opt -Oz -g,
  which exercises both LLVM outermost-name failure modes)
- Trap inside loop body (DWARF line-table edge case)
- Multi-frame call stack
- C++ symbol demangling
- AOT mode offset math
- fast-interp / --no-addr fallbacks
- offset=0 fallback (trap at function entry)
- Empty input
- Skip-guard invariant on build_subprogram_intervals: rejects
  DW_AT_declaration=true DIEs, low_pc == 0 (wasm-opt DCE ghosts),
  and high_pc <= low_pc entries at parse time.

Multi-SDK mode (--multi-sdk) parametrizes build-based tests over every
detected wasi-sdk under /opt to catch DWARF-encoding differences
between clang versions.

## CI

compilation_on_android_ubuntu.yml:
- Existing build_samples_others step (debug-tools) now calls verify.sh
  instead of inline grep assertions.
- New build_samples_debug_tools_optimized job: builds the new sample
  twice (classic-interp and fast-interp) and runs verify.sh
  oob/stackoverflow × wasm/aot for each.
- New addr2line_tests job: runs the pytest suite against the default
  wasi-sdk on every PR.

nightly_run.yml:
- New addr2line_tests_multi_sdk job: installs wasi-sdk 29.0 (clang 21)
  and 33.0 (clang 22) side by side and runs the test suite with
  --multi-sdk.
@BenSunWhiteBoard BenSunWhiteBoard force-pushed the dev/addr2line_enhance branch from 8f161d0 to 8f033d9 Compare July 7, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant