From 5b1567c49f3e53e57433bc472e5fca8a8bb0f98e Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 14 Sep 2026 10:34:29 +0700 Subject: [PATCH] The transient thunk, so an expression has something to compile to Item 2 of HANDOFF-x86-redef.md. C-x C-e and every value a break loop computes are not a redefinition: there is no name to install a body into, so the expression is wrapped in a function with nowhere to be called from and the module says "run this once". flan_reload_call is that wrapper, and the agent dlsyms exactly that spelling. Its shape is emit_main's rather than a body's: no caller hands it a transfer channel, so it owns a null cell on its own frame and passes that cell's address on. Sixteen bytes of frame rather than eight, because rsp has to be 16-aligned at the call and that is the whole of what the ABI asks of a frame making one. The thunk itself is excluded from everything else the module does -- no cell, no publish, no registry slot. There are 4096 slots and an expression evaluated in a loop would exhaust them, and a module with nothing pointing into it is what lets the agent unload it at all. @flan_reload_transient is that claim, under emit.ml's three conditions. The third is about data rather than text and is the one that can be got wrong in the dangerous direction: a string literal lives in this module's image, an expression may store one anywhere it likes, and a global left pointing into an unmapped image is silent garbage rather than a fault. So the count is kept where the literals are made -- string_const bumps the same Emit.m.nstr field emit.ml counts on, and float constants, which are loaded and never retained, deliberately do not. The install function's own registry name strings go through string_const too, which is right rather than incidental: a module that interned a name left something behind. --- lib/x86.ml | 105 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/lib/x86.ml b/lib/x86.ml index 779c1e8..22a478b 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -696,7 +696,15 @@ let escape_bytes s = (List.map (fun c -> Printf.sprintf "0x%02x" (Char.code c)) (List.init (String.length s) (String.get s))) +(* Counted, on the same field [emit.ml] counts it on and for the same one + reason: it is the test [redefinition] applies before it lets an expression + thunk's module say it may be unloaded. A string literal is emitted into this + module's image and the expression may store it anywhere it likes, so a + module holding one keeps its mapping. A float constant is a label in the + same section and is deliberately not counted -- it is loaded, never + retained. *) let string_const f s = + f.md.Emit.nstr <- f.md.Emit.nstr + 1; let l = rodata_label f in Buffer.add_string f.rodata (Printf.sprintf "\t.align 1\n%s:\n" l); if String.length s > 0 then @@ -3411,16 +3419,17 @@ let program ~checks ?(dev = false) ?(debug = false) (p : Tast.program) : string transient [flan_reload_call] thunk is not built here, and is refused by name -- this file's idiom for a case it has not earned the right to compile. *) let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) - ?(consts = []) ?call (p : Tast.program) ~fns : string = + ?(retains = true) ?(consts = []) ?call (p : Tast.program) ~fns : string = if not dev then unsupported "x86 redefinition without cells: there is nothing to publish a body \ into, and this backend's release build has no indirection"; - (match call with - | Some _ -> - unsupported - "x86 redefinition: the transient flan_reload_call thunk is not built yet" - | None -> ()); + (* A thunk this module runs itself is excluded from all of the machinery + below: [flan_reload_call] calls it directly, so it needs no cell, must not + be published into one, and must not take a registry slot -- there are 4096 + of those and an expression evaluated in a loop would exhaust them. + Nothing pointing into the module is also what lets the agent unload it. *) + let transient n = call = Some n in let target name = match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = name) p.Tast.fns with | Some f -> f @@ -3445,7 +3454,11 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) function's slot holds its cell's address and a global's holds its storage's, so the two are the same eight bytes and differ only in what fills them. *) - let new_fns = List.filter (fun (f : Tast.fn) -> not (known f.Tast.name)) siblings + let new_fns = + List.filter + (fun (f : Tast.fn) -> + (not (known f.Tast.name)) && not (transient f.Tast.name)) + siblings and new_globals = List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) p.Tast.globals @@ -3565,13 +3578,17 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) pc-relative [lea]. *) List.iter (fun (fn : Tast.fn) -> - if known fn.Tast.name then - load_int f.b ~dst:rax ~mm:(Got (csym fn.Tast.name)) ~size:8 ~signed:false - else - load_int f.b ~dst:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8 - ~signed:false; - lea f.b ~dst:r11 ~mm:(Sym (fsym fn.Tast.name, 0)); - store_int f.b ~src:r11 ~mm:(Reg (rax, 0)) ~size:8) + if transient fn.Tast.name then () + else begin + if known fn.Tast.name then + load_int f.b ~dst:rax ~mm:(Got (csym fn.Tast.name)) ~size:8 + ~signed:false + else + load_int f.b ~dst:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8 + ~signed:false; + lea f.b ~dst:r11 ~mm:(Sym (fsym fn.Tast.name, 0)); + store_int f.b ~src:r11 ~mm:(Reg (rax, 0)) ~size:8 + end) targets; (* Nothing a constant initialiser can do transfers, so this exit is unreachable and is emitted only when something claims to aim at it. *) @@ -3600,6 +3617,38 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) Buffer.add_buffer text f.b.out; Buffer.add_string text "\t.size\tflan_reload_install, . - flan_reload_install\n\n"; + (* An expression evaluation compiles to a function with nowhere to be called + from, so the module says so and the agent runs it once -- after the + install, on the game thread, so it sees both the bodies this module just + published and a program state the program agrees is consistent. + + The thunk takes no parameter but the transfer channel, and no caller hands + this wrapper one, so it owns a null cell on its own frame exactly as + [emit_main] does. Sixteen bytes rather than eight keeps rsp 16-aligned at + the call, which is the whole of what the ABI asks of a frame that makes + one. *) + (match call with + | None -> () + | Some fn -> + let cb = create () in + push_r cb rbp; + mov_rr cb ~dst:rbp ~src:rsp; + sub_imm cb ~dst:rsp 16; + xor_rr cb ~dst:rax ~src:rax; + store_int cb ~src:rax ~mm:(Frame (-8)) ~size:8; + lea cb ~dst:rdi ~mm:(Frame (-8)); + xor_rr cb ~dst:rax ~src:rax; + call_sym cb (fsym fn); + leave cb; + ret cb; + flush cb; + Buffer.add_string text + "\t.globl\tflan_reload_call\n\ + \t.type\tflan_reload_call, @function\n\ + flan_reload_call:\n"; + Buffer.add_buffer text cb.out; + Buffer.add_string text + "\t.size\tflan_reload_call, . - flan_reload_call\n\n"); Buffer.add_buffer rodata f.rodata; let out = Buffer.create 8192 in Buffer.add_buffer out text; @@ -3640,6 +3689,34 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) Buffer.add_string out (Printf.sprintf "\n\t.data\n\t.align\t8\n%s:\n\t.quad\t%s\n" (asm_sym "flan.abi.require") (asm_sym abi_marker)); + (* Nothing outside this module refers to anything in it once the call has + returned -- no cell holds an address in its text, the registry has no slot + for it, and the value it produced was copied out. So it says so, and the + agent [dlclose]s it. A module that publishes a body can never say this: + its whole purpose is to leave a pointer behind. + + [nstr = 0] is the third condition and it is about data, not text. A string + literal is emitted into this module's own image, and an expression may + store one anywhere it likes -- [(set msg "tuned")] on a string global + leaves that global pointing into the mapping the agent is about to drop. + The next thunk can be mapped at the same address, so the result is silent + garbage rather than a fault. A module with no string constants has nothing + in its image anyone could still be pointing at; one with any keeps its + mapping, which costs a page and is the same bargain every redefinition + already makes. [string_const] is where the count is kept, and the install + function's own registry names go through it too -- which is right rather + than incidental, since a module that interned a name left something + behind. *) + (match call with + | Some fn + when fns = [ fn ] && consts = [] + && ((not retains) || md.Emit.nstr = 0) -> + Buffer.add_string out + "\n\t.data\n\t.globl\tflan_reload_transient\n\ + \t.type\tflan_reload_transient, @object\n\ + \t.size\tflan_reload_transient, 1\n\ + flan_reload_transient:\n\t.byte\t1\n" + | _ -> ()); Buffer.add_string out "\n\t.section\t.rodata\n"; Buffer.add_buffer out rodata; Buffer.add_string out "\n\t.section\t.note.GNU-stack,\"\",@progbits\n";