diff --git a/HANDOFF-x86-debug.md b/HANDOFF-x86-debug.md index f511d04..aa579e9 100644 --- a/HANDOFF-x86-debug.md +++ b/HANDOFF-x86-debug.md @@ -43,7 +43,7 @@ which is the same thing this backend already does for instructions. **`.cfi` is the opposite finding and it is good news.** GAS's CFI machinery computes its advances from frag positions and not from `md_assemble`, so `.cfi_def_cfa_offset` and friends interleaved with `.byte` come out *exactly* right — measured, `readelf --debug-dump=frames` on a `.byte`-only function gives the correct -advances. Nothing here emits them yet (see "What was not reached"), but the next lane should know it can. +advances. So this lane emits them, in a `--debug` build — see below. ## What landed @@ -86,6 +86,7 @@ In `lib/x86.ml`: | `emit_fn ?dw` | opens the subprogram, seeds it with a row at the function symbol on the `defn`'s line, closes it with a label one past the last byte | | `emit_dwarf` | `.debug_abbrev` (two abbreviations), `.debug_info` (one CU, one `DW_TAG_subprogram` per function), `.debug_line` (header, directory table, file table, program) | | `program ~debug` | a numbered `.file` at the top, `.Ldwtext` / `.Ldwtext_end` around the text, and the sections at the end | +| `cfi_after_push` / `cfi_after_mov` / `cfi_after_leave`, above `emit_fn` | the whole frame model in five directives, at the three sites that share a prologue: `emit_fn`, `emit_main`, `emit_globals_init` | Two decisions worth knowing about because they are not obvious: @@ -157,19 +158,64 @@ project's own source-level debugging case runs under **lldb**, which does not ha debug.flan:19` and `break flan.tick` both work everywhere. Matching `emit.ml` was chosen over diverging from it; if this is worth fixing it should be fixed in both places at once. +**lldb**, which is the debugger this project's own source-level case runs under, does better still — it +resolves `flan.tick` by name, shows the source inline and gives a column: + +``` +$ lldb -b -o "breakpoint set --name flan.tick" -o run -o bt ./dbg +* thread #1, stop reason = breakpoint 1.1 + frame #0: 0x0000000000400647 dbg`flan.tick at debug.flan:18:3 + 17 (defn tick [c (Ptr Cell) n i32] i32 +-> 18 (let [bump (+ n 1)] + ^ + 19 (set (.heat c) (+ (.heat c) 1.5)) +(lldb) bt + * frame #0: 0x0000000000400647 dbg`flan.tick at debug.flan:18:3 + frame #1: 0x00000000004007b5 dbg`flan.main at debug.flan:25:28 + frame #2: 0x0000000000400bea dbg`main + 41 + frame #3: 0x00007ffff7cb9575 libc.so.6`__libc_start_call_main + 117 +``` + +That transcript is now a test rather than a transcript: `test/test_acceptance.ml`'s lldb block has an `--x86` +arm beside its `--dev` one, asserting the breakpoint resolves and both Flan frames name `debug.flan`, and that +the program still prints what it printed. It claims no `frame variable`, which is the gap. + +**Unwinding out of the C runtime works**, which is the case that matters for this project's error paths and +which the brief did not ask for but should have. `bounds.flan` built `--x86 --debug`, breaking on the C symbol: + +``` +Breakpoint 1, flan_bounds_error (loc=0x40a880 "test/programs/bounds.flan:11:44", ...) at flan_rt.c:534 +#0 flan_bounds_error (...) at flan_rt.c:534 +#1 0x00000000004006be in flan.main () at bounds.flan:11 +#2 0x000000000040162e in main () +``` + +**The compile unit's range is exact.** `DW_AT_low_pc` 0x400628 is `flan.tick`, the first function; +`DW_AT_high_pc` 0x5cf puts the end at 0x400bf7, and `nm -S` says `main` — the last thing this object puts in +`.text` — ends at 0x400bc1 + 0x36 = 0x400bf7. So `.Ldwtext` / `.Ldwtext_end` bracket the four functions this +unit emitted and nothing else. + +`--x86 --dev --debug` together, which is a reachable combination and was never exercised by anything else, +builds, runs and gives a line table `readelf` reads with no warnings. + Cross-checked on a spread of the corpus — `debug`, `generics`, `vec`, `maps`, `strings`, `conditions`, -`bounds-condition`, `algorithms`, `handles`, `registry` — all build with `--x86 --debug`, run, and produce -`.debug_line` that `readelf --debug-dump=decodedline` reads with **zero warnings**. `generics` is the +`bounds-condition`, `algorithms`, `handles`, `registry`, `destructure`, `edn`, twelve in all — every one +builds with `--x86 --debug`, runs, and produces `.debug_line` that `readelf --debug-dump=decodedline` reads +with **zero warnings**. `edn` and `generics` are the large ones, at 40 and 36 subprograms. `generics` is the multi-file case: its directory table has one entry and its file table two, `generics.flan` and ``. `` has no path on disk and a debugger simply finds no source for it, which is the truth. ## Baseline, measured on this tree -| | before | after | +| | before (`957ba07`) | after | |---|---|---| -| `spike/x86/survey.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86, skips 28 + 6 + 2 | (see below) | -| `spike/x86/cells.sh` | 4/4 ok | (see below) | -| `dune test --root .` | exit 0 | (see below) | +| `spike/x86/survey.sh` | 99 MATCH / 0 DIFFER / 0 REFUSED / 0 NOX86, skips 28 + 6 + 2 | **99 / 0 / 0 / 0**, skips **28 + 6 + 2** — identical | +| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** | +| `dune test --root .` | exit 0 | **exit 0**, run without a pipe; acceptance reports 232 checks, 0 failures | + +The survey is protected structurally as well as measured: every piece of this lane's work on the release path +is behind `debug`, so `--x86` without `--debug` emits the assembly it emitted before — `fnctx.dw` is `None`, +`dwline` does nothing, the `.Ldwtext` label is not written and neither are the `.cfi` directives. Run `dune test --root .` **without a pipe** — `HANDOFF-x86-redef.md` is emphatic and right: piping gives you `tail`'s exit status, and the `dev-robust` fixture puts `ld` and `clang` failure text in the output either way @@ -187,14 +233,18 @@ while the run still exits 0. reachable: `DW_TAG_formal_parameter` and `DW_TAG_variable` for `snames`-named slots only, with `DW_AT_frame_base` as `DW_OP_call_frame_cfa` or `DW_OP_reg6`. That needs a type-DIE emitter, which is the part `emit.ml` spends most of its debug lines on. -2. **`.cfi`, and it is now known to be cheap.** Nothing here emits it, so gdb unwinds by its prologue - analyser — which works, because `push rbp; mov rbp,rsp; sub rsp,N` is the canonical pattern and this - backend's header guarantees `rsp` is written exactly twice. The backtraces above are that analyser - working. But the experiment at the top of this file says `.cfi` against `.byte` output is exact, and the - frame model makes the content textbook: CFA is `rsp+16` after the `push`, `rbp`-based after the `mov`, and - unchanged for the whole body. It would want `.cfi_startproc` / `.cfi_endproc` in `emit_fn` and three - directives in the prologue. Worth doing; it is what makes a backtrace survive an unwind from inside the - runtime's C. +2. **`.cfi` is emitted, but only in a `--debug` build.** Five directives at three sites — `emit_fn`, + `emit_main`, `emit_globals_init`, which share a prologue. `readelf --debug-dump=frames` gives the exact + advances. `emit_main` gets no closing rule because it has no epilogue: it leaves through `flan_exit` and + the `ud2` after that is unreachable, so the `rbp` rule holds to the last byte. + + gdb did not need it — its prologue analyser already unwound out of `flan_bounds_error`, because + `push rbp; mov rbp,rsp; sub rsp,N` is the pattern it recognises — so this is a description stated rather + than guessed, plus a correct unwind from the very first byte of a function, before the `push`. + + **The open decision is whether to make it unconditional.** The description is correct in every build and a + release build is where a crash would most want it. What stopped it here is only that nothing measures the + `.eh_frame` it would add, and another lane is measuring backend cost right now. One `if` in three places. 3. **`X86.redefinition` emits no debug information.** It is passed no `dwarf`, so a redefinition module's bodies have no lines. Nothing reaches that yet — `flan dev` does not build `--x86` at all (`HANDOFF-x86-redef.md` §"What remains" item 3) — but when it does, a break loop stopping in a reloaded @@ -203,9 +253,10 @@ while the run still exits 0. `high_pc` range, which is honest — a debugger finds no line for an address in them and says so — but a backtrace through the globals' initialiser names nothing. `emit_globals_init` does lower expressions that carry locations, so this is a small piece of work: give it a `dwsub` the way `emit_fn` has one. -5. **Nothing tests this in `dune test`.** The corpus's source-level debugging case (`test/programs/debug.flan` - and `debug-permuted.flan`) runs under lldb against the LLVM backend. An `--x86` arm of it would be the - right regression test and does not exist; the verification above is a transcript in a handoff, which rots. +5. **`debug-permuted.flan` has no `--x86` arm.** The new acceptance case covers `debug.flan` only. The + permuted fixture exists to catch a member offset that does not follow the declaration, and that is a + *types* claim — there are no type DIEs here to get wrong, so it would test nothing today. It becomes the + right test the moment item 1 above is attempted, and it is already written. ## Open questions for the author