Skip to content

Loader: stubs lack .eh_frame / unwind info — debugger and exception unwinding broken through stubs #10

@scc-tw

Description

@scc-tw

Summary

The three StubEmitter implementations (X86_64, X86_32, ARM64) generate machine code only — no DWARF CFI (Call Frame Information) entries. The injected .vmpilot section has no corresponding .eh_frame (ELF) or __unwind_info (Mach-O).

Priority: Post-first-release. v1 error policy is abort() — no C++ exceptions cross the stub boundary. This becomes critical in v2 when NATIVE_CALL targets may throw.

Impact

Scenario Result
Debugger backtrace gdb/lldb cannot unwind through the stub; bt terminates or shows garbage frames
C++ exception through native call If a NATIVE_CALL target throws, the unwinder finds no FDE for the stub PC, falls back to heuristics, reads wrong RSP offset → std::terminate() or segfault
Profiler (perf, VTune) Stack sampling breaks at stub → incomplete profiles
Crash dump analysis Coredump/minidump cannot reconstruct the call chain through the stub

Technical Details

Each stub's stack frame (x86_64 example):

[ENDBR64]            ← 4 bytes (CET landing pad)
[PUSH rbx..r15]      ← 6 × 8 = 48 bytes (callee-saved)
[initial_regs array] ← sub rsp, 128 (16 × 8)
[VmStubArgs (64B)]   ← sub rsp, 64
[CALL vm_stub_entry] ← indirect call through call_slot
[ADD rsp, 64+128]    ← deallocate
[POP r15..rbx]       ← restore
[JMP resume]         ← branch to region end

Required .eh_frame FDE description:

  • Initial CFA = rsp + 8 (return address)
  • After each PUSH: CFA offset increments by 8, register saved at [CFA - N]
  • After sub rsp, 128: CFA offset += 128
  • After sub rsp, 64: CFA offset += 64
  • At CALL: all callee-saved locations described

Implementation Plan

  1. StubEmitter: emit_entry_stub() returns a new Stub::eh_frame_fde field (raw FDE bytes) alongside the machine code. The FDE offsets are relative to the stub start.

  2. PayloadBuilder: Collect all per-stub FDEs, prepend a shared CIE (Common Information Entry), build a complete .eh_frame section.

  3. ELFEditor: When add_segment() is called, also create an .eh_frame section (SHT_PROGBITS, SHF_ALLOC) containing the CIE+FDE data. Register it with a PT_GNU_EH_FRAME segment if needed.

  4. MachOEditor: Create compact unwind entries or __eh_frame section within the __VMPILOT segment.

  5. PEEditor: Create .pdata (function table) + .xdata (unwind data) for x64 SEH.

Why Not Now

  • v1 error policy: vm_stub_entry calls abort() on any error (NDEBUG) or returns INT64_MIN (debug). No C++ exception crosses the stub boundary.
  • DWARF CFI binary encoding is complex (ULEB128, register mappings per ABI, CIE augmentation strings). Getting it wrong is worse than not having it.
  • Debugger backtrace quality is a developer convenience, not a correctness issue.

References

  • D15 §5.2: Uniform 12-step pipeline (stubs are pre-pipeline entry points)
  • D10 GAP5 (C5): "Exception/stack unwinding — stubs break DWARF unwinding"
  • RM §5: "Register CFG/CFI/BTI valid targets" (partially done: landing pads present, unwind info missing)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions