← board

DWARF debug info (-g) — phased, x86-64 first

Motivation

PXX emits no debug info of any kind today — no .debug_*, no stabs. A PXX binary is opaque to gdb/lldb: no line numbers, no function names in backtraces, no variable inspection. As programs (and the compiler itself) grow, the absence of a real debugger is the main productivity gap. Target DWARF (debug DWARF3 as the nominal version; gdb consumes 2/3/4/5 and the line program barely differs across them).

Key feasibility fact: PXX is its own linker

PXX writes the final ET_EXEC directly (own program headers, own dynamic tables, picks the ld.so interpreter itself — elfwriter.inc). There is no GNU as/ld in the final path. Therefore nothing downstream can emit DWARF for us — PXX must write every .debug_* section byte-by-byte. Full burden, but full control and zero new toolchain dependency. The section-emission machinery (shstrtab, section headers, symtab) already exists in elfwriter.inc to extend.

Current state (examined 2026-06-20)

Plan — three burden tiers, ship as separate sub-tickets

Tier 1 — .debug_line (address → file:line). Highest value/effort ratio.

Delivers gdb breakpoints, single-step, Ctrl-C-shows-location, and line numbers in backtraces. This tier alone = a usable debugger.

Tier 2 — .debug_info + .debug_abbrev + .debug_str: subprograms.

DIEs for the compile unit + one DW_TAG_subprogram per function (name, low_pc/high_pc from Procs[].BodyAddr, frame-base). → function names in bt, proper frames. No type system needed yet.

Tier 3 — locals / params / types. The long tail, incremental.

DW_TAG_variable / DW_TAG_formal_parameter with location expressions (frame-base + slot offset), and the type DIE graph mapping the tyXxx model + records/classes/dynarrays/managed strings/variants to DW_TAG_base_type/pointer_type/structure_type/array_type. Enables print x and struct inspection. Largest piece; land type kinds one at a time.

Cross-target (×5, deferred until x86-64 proven)

DWARF content is target-independent except: PC/address values, the frame-base register (rbp / x29 / r11 / …) via the per-ABI DWARF register-number map, and pointer width (DWARF32 form on the ELF32 targets i386/arm32/riscv32/ xtensa). One emitter + a small per-arch register/width table. Do x86-64 end to end first; generalise after.

Gates / interactions

Build-out order (split when work starts; do not pre-flood the board)

  1. Tier 1 .debug_line, x86-64 only — prove gdb steps a PXX binary, breakpoints hit, bt shows lines.
  2. Tier 2 subprograms (function names / frames).
  3. Tier 3 locals + base types, then aggregate types incrementally.
  4. Cross-target generalisation.

Acceptance

Re-validation 2026-06-24 (all claims still hold) + plan refinements

Code re-checked against current tree. Findings that change the estimate:

Graceful-degradation decision (kills the Tier-3 bulk)

Anything with runtime/heap layout is emitted as a labeled DW_TAG_pointer_type, NOT a byte-exact ABI struct DIE:

What stays "real" (all cheap, RTTI-backed): base types (DW_TAG_base_type), records/classes (DW_TAG_structure_type + members from the EXISTING RTTI: __rttireg / UClsRTTIOff / typinfo — do NOT rebuild field metadata), fixed arrays (DW_TAG_array_type). The expensive part — describing dynarray/string/ variant runtime layout byte-exact — is DELETED. Inspection works for ints, records, class fields, fixed arrays; the hard 10% shows a labeled pointer instead of nothing.

Refined effort: Tier 1 (line + CU stub) ≈ 1 session; Tier 2 (subprograms/frames) small; Tier 3-lite (base types + RTTI structs + pointer-cheat) ≈ 1 session.

Log