Skip to content

feat(dwarf): DW_TAG_subprogram DIEs — function names in backtraces (#394) - #557

Merged
avrabe merged 1 commit into
mainfrom
fix/394-subprogram-dies
Jul 1, 2026
Merged

feat(dwarf): DW_TAG_subprogram DIEs — function names in backtraces (#394)#557
avrabe merged 1 commit into
mainfrom
fix/394-subprogram-dies

Conversation

@avrabe

@avrabe avrabe commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What

VCR-DBG-001 Tier-1 increment for #394. synth already emits a real
.debug_line (addr→file:line) on the ARM --relocatable path behind
--debug-line, and the CU root DIE carries name/low_pc/high_pc/stmt_list.
This PR adds the missing piece: per-function DW_TAG_subprogram child
DIEs
, so a gdb/lldb backtrace shows the function name instead of a
bare address.

Before → after

  • Before: the emitted DW_TAG_compile_unit had a DW_AT_stmt_list
    line table but zero children. A debugger resolved a .text address
    to file:line, but printed no function frame.
  • After: one DW_TAG_subprogram child DIE per compiled function:
    • DW_AT_name = the export/function name (synth already holds it in
      ElfFunction.name).
    • DW_AT_low_pc = the function's .text address, relocated against the
      same __synth_text_base symbol as the CU low_pc (addend = the
      function's object-relative .text offset) — one extra
      .rel.debug_info R_ARM_ABS32 record each, so it shifts correctly
      when a linker places .text.
    • DW_AT_high_pc = offset (size) form high_pc - low_pc — a plain
      constant, no relocation (matches the CU's high_pc form).

No parameters/locals/frame-base yet — that is Tier-2, gated on VCR-RA.

Files

  • crates/synth-core/src/dwarf_line.rs — new SubprogramInfo struct;
    emit_debug_sections takes &[SubprogramInfo] and adds the child DIEs
    under the CU root.
  • crates/synth-cli/src/main.rs (DWARF compose block) — builds the
    subprogram list from func.name + func_offsets[i]..+code.len() and
    threads it in. Reloc/symbol wiring is unchanged — the extra .text
    relocs flow through the existing per-section .rel.debug_* path.
  • crates/synth-cli/tests/dwarf_debug_line_emit_394.rs — new Oracle F;
    Oracle C tightened.

Purely additive (frozen-safe)

Subprogram low_pcs reuse __synth_text_base via an addend, so no new
symbol
is defined. .text/.data/.bss stay byte-identical with vs
without --debug-line. Frozen frozen_codegen_bytes 3/3; Oracle A
(byte-identity) green; DWARF anchors did not shift.

Oracle: red → green

New Oracle F (emitted_debug_info_has_subprogram_dies_per_function_394):
walks the emitted CU the debugger way (dwarf.units() → CU tree) and asserts

  1. one DW_TAG_subprogram per func_N symbol the object defines;
  2. every exported wasm function name — derived from the input's export
    section at runtime (non-circular, mirroring Oracle E's filename
    check), e.g. z_impl_k_msgq_put, gale_k_msgq_put_decide — appears as
    a DW_AT_name;
  3. every [low_pc, high_pc) lies within .text.
  • RED on pre-feature code, for the right reason: "emitted CU has NO
    DW_TAG_subprogram children"
    .
  • GREEN after the emit + threading.

Oracle C tightened, not relaxed. .debug_line still carries exactly
one reloc (addend 0). .debug_info now carries exactly 1 + N
R_ARM_ABS32 relocs vs __synth_text_base (CU low_pc + one per
subprogram), and the multiset of in-place addends must equal
{0} ∪ {each subprogram low_pc} — proving each subprogram low_pc
relocates by the base to text_base + low_pc.

Gates (exit-code verified)

  • cargo test -p synth-cli --test dwarf_debug_line_emit_3947/7 (A–F).
  • cargo test -p synth-cli --test frozen_codegen_bytes3/3.
  • cargo test --workspace --exclude synth-verify — exit 0 (synth-verify
    excluded due to the local z3-sys build issue).
  • cargo fmt --check clean; cargo clippy --workspace --all-targets -- -D warnings clean.

Honest scope

Internal (non-exported) functions carry func_N as DW_AT_name,
even though the input wasm's name custom section has richer names
(core::panicking::…, gale::msgq::put_decide::…). This matches the
task scope ("func_3 / the export name") and the data ElfFunction.name
already holds. Propagating name-section names for internals is a natural
Tier-1.x follow-up. Subprograms are emitted only when the unit is
emitted at all (≥1 line row), consistent with the pre-existing additive
no-op contract.

🤖 Generated with Claude Code

…names (#394)

VCR-DBG-001 Tier-1 increment. The emitted CU previously carried a
DW_AT_stmt_list line table but ZERO children, so gdb/lldb resolved a
`.text` address to `file:line` yet printed no function frame. Attach one
`DW_TAG_subprogram` child DIE per compiled function:

  - DW_AT_name    = the export/function name (synth already has it).
  - DW_AT_low_pc  = the function's `.text` address, relocated against the
    SAME `__synth_text_base` symbol as the CU low_pc (addend = the
    function's object-relative offset) — one extra `.rel.debug_info`
    record each, so it shifts correctly when a linker places `.text`.
  - DW_AT_high_pc = offset (size) form `high_pc - low_pc`, no relocation.

No parameters/locals/frame-base yet — that is Tier-2, gated on VCR-RA.

Purely additive: subprogram low_pcs reuse `__synth_text_base` via an
addend, so no new symbol is defined and `.text`/`.data`/`.bss` stay
byte-identical (frozen 3/3, Oracle A green).

New Oracle F (red→green): walks the emitted CU the debugger way, asserts
one subprogram per `func_N` symbol, every exported wasm function name
(derived from the input's export section at runtime — non-circular)
present as a DW_AT_name, and each `[low_pc,high_pc)` within `.text`.
Oracle C extended: `.debug_info` now carries 1 (CU) + N (subprogram)
R_ARM_ABS32 relocs vs `__synth_text_base`, each in-place addend equal to
that function's object-relative low_pc (relocates by the base).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 22bc4c5 into main Jul 1, 2026
25 checks passed
@avrabe
avrabe deleted the fix/394-subprogram-dies branch July 1, 2026 21:10
avrabe added a commit that referenced this pull request Jul 2, 2026
…me section (#394) (#565)

Tier-1.x follow-up to #557: DW_TAG_subprogram DW_AT_name for INTERNAL
(non-exported) functions was the synthetic func_N because the wasm
`name` custom section (function-names subsection) was never parsed.
A backtrace through a panic showed `func_7` instead of
`core::panicking::panic_const::panic_const_add_overflow::h...`.

- wasm_decoder: parse the `name` custom section (KnownCustom::Name →
  Name::Function map, best-effort — debug metadata must not fail a
  compile) into a new `FunctionOps.debug_name`, applied after the parse
  loop (the section trails the code section). Both decode_wasm_module
  and decode_wasm_functions.
- CLI: thread `debug_name` through ElfFunction into the subprogram
  compose. Name priority: name-section > export name > func_N.
  DWARF-only — the symbol table and relocation labels keep using the
  export-name-or-func_N `name`, so linkability and emitted bytes are
  unchanged.
- Oracle G (dwarf_debug_line_emit_394): ground truth parsed from the
  fixture at runtime; asserts ≥1 internal function's DIE carries its
  real name-section name and zero synthetic func_N names remain
  (msgq_put_359 names every function). RED before (func_7/8/10),
  GREEN after.

Frozen-safe: purely additive DWARF metadata — frozen_codegen_bytes 3/3,
.text byte-identical.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Jul 2, 2026
…) (#573)

The compile_unit DIE's DW_AT_high_pc was derived from the LINE-TABLE
extent (last mapped address + 1), while the child DW_TAG_subprogram DIEs
(#557, v0.19.0) carry true CODE extents. A function's code routinely
extends past its last line-mapped address (the mapped op itself is >=2
bytes, plus any unmapped epilogue/literal-pool tail), so the tail
function fell OUTSIDE its parent CU's range: llvm-dwarfdump --verify
fails post-link with "DIE address ranges are not contained in its
parent's ranges", and a debugger walking CUs by PC range misses it.

Fix: the CU DW_AT_high_pc now covers max(subprogram high_pc) - the
.text code extent the compose already passes - falling back to the
line-table extent when no subprograms exist. The line program's
end_sequence is untouched (.debug_line bytes unchanged).

Oracle H (new, red->green): parse the emitted CU with gimli::read and
assert [CU low_pc, CU high_pc) CONTAINS every subprogram's
[low_pc, high_pc) - the llvm-dwarfdump containment rule, checked
in-tree. Red on main with msgq_put_359.wasm: CU [0x0,0x453) failed to
contain panic_fmt [0x450,0x456). Green after: CU [0x0,0x456). Includes
a non-vacuity guard (code extent > line extent) so the fixture keeps
exercising the #564 shape.

Confirmed with llvm-dwarfdump --verify on the issue's exact repro
(dwarf_coherent.wasm linked at -Ttext=0x8000): "No errors"; CU high_pc
now equals clampi's high_pc.

Frozen codegen bytes 3/3 green (DWARF additive); oracles A-G green;
fmt + clippy clean.

Closes #564

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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