5.7 KiB
Handoff — a redefinition emitter in lib/x86.ml
Branch dev-loop, worktree agent-a884e1e2fcffe5052, starting from b1cc67b.
Item 1 of HANDOFF-x86-rt.md §6. Written early, before the work, because the session's budget is short;
each section is updated as the work lands, and a section marked not done is honestly not done.
The baseline this must not regress
Measured at the start of this session, on b1cc67b:
spike/x86/survey.sh— (filled in below once the run finishes)dune test --root .— check the exit code, not the output. It prints twold/clangfailures from inside thedev-robustfixture and still exits 0. Grepping the output will mislead you.
Why this cannot be skipped by leaning on LLVM
x86.ml's header licenses its own calling convention on the grounds that a dev build is compiled entirely by it
and a release build entirely by LLVM, and the two never meet in one process. The conventions agree on scalars
and disagree on every aggregate (here: every aggregate by pointer with a hidden sret in the first integer
register; LLVM classifies by eightbyte). An Emit.redefinition module dlopened into an --x86 host is correct
until the first redefined function takes or returns a struct. So an --x86 host must get --x86 redefinition
modules. Do not write an aggregate classifier.
The crux: PIC references to the host's symbols
program emits a whole executable; every symbol it names is either its own or the runtime's, and a
rip-relative PC32 reference is right for all of them. A redefinition module is a .so and almost every symbol
it names belongs to the host executable:
- the cells
flan.cell.<n>(data) - the globals
flan.<g>(data) - the sibling function bodies, when taken by address
A PC32 relocation against an undefined symbol in a -shared link fails with
relocation R_X86_64_PC32 against undefined symbol ... can not be used when making a shared object.
Data references have to go through the GOT (sym@GOTPCREL, load the address, then dereference).
Calls are fine as call sym — they get a PLT entry.
This is exactly what llc -relocation-model=pic does for Emit.redefinition's
@"flan.cell.x" = external global ptr. Get the spelling from llc, not from recall: compile a three-line
.ll with an external global and a call through a loaded ptr, read the asm, and confirm end to end with
as → ld -shared → readelf -r (want R_X86_64_GOTPCREL, not PC32).
Plan
- Additive addressing. A new
locconstructorLgot of string * int— a symbol reached through the GOT — handled inshift,lmem(load the GOT slot intoscratch, thenReg (scratch, d)— theLpshape) andaddr_into.Lgkeeps its exact meaning, so the whole-program path emits byte-identical output and the survey is protected structurally rather than by re-measurement. Ashared : boolonfnctx(default false) is what selects between them. - Every non-local PC-relative site. Not just loads — the
leasites too. From the grep:lib/x86.ml:944/:956—FnAddr (Flanfn n)/ the cell read forFnvallib/x86.ml:959—leaof an FFI/extern symbollib/x86.ml:1463/:1574—Tast.Global/Tast.Pgloballib/x86.ml:1766—`Cellincall_flanLocals stayPC32and must: string literals (:922,:1129) and lifted-clause addresses (:1157) are defined in the module.
X86.redefinition. What it must not emit, each of whichprogramdoes and each of which is wrong in a module: nomain/emit_main; no.init_arrayand noinit_sym— re-running the globals initialiser wipes the live state reloading exists to preserve; noflan_dev_reg_enablector; noemit_globals_data(host globals are undefined references); noemit_cells(the cells are undefined references). What it must emit:.hiddenon each target body — default visibility in a.sois interposable, so a plain reference would resolve to the host's copy and the module would install the very body it is replacing (emit.ml:2072says this is load-bearing) — and aflan_reload_installfunction, unquoted and not hidden, which is whatreload_host.c:72andvendor/agent/flan_agent.c:1128dlsym.- Scope cut, deliberate. Build the known-name path only: redefining functions the host already has,
referring to globals the host already has. Refuse by name the rest of
Emit.redefinition's surface — new functions (flan_dev_cell+Emit.cellptrslot), new globals (flan_dev_global+ slot + init constant),consts, and the transientcallthunk. That is this file's existing idiom (call_native,check_no_transfer): a refusal with the reasoning written down beats a half-built path nothing can run. Redefining an existingdefnisC-c C-c, is the demo, and is whatp8-cell.flancan test. - Assembling it.
Build.sharedisllc+ld -shared. The x86 counterpart isas+ld -sharedon the same output;-sharedis the part that matters. - Verify by running. Item 15: a disassembly that reads correctly beside a wrong answer is the normal
outcome of hand-encoding. The harness to copy is
test/test_reload.ml+test/reload_host.c— build the host with{ dev = true; x86 = true }(Build.executableaccepts both; the--x86refusal only names wasm,--debugand--sanitize), build the module withX86.redefinition, and check the printed answers change.spike/x86/cells.shis the smaller check that already exists.
Status
- Oriented; plan above; baseline running.
llcprobe for the GOTPCREL spelling.Lgotaddressing.X86.redefinition.- Assembling + a test that runs it.