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.
This commit is contained in:
Joseph Ferano 2026-09-14 10:34:29 +07:00
parent 5a67cf458a
commit 5b1567c49f

View File

@ -696,7 +696,15 @@ let escape_bytes s =
(List.map (fun c -> Printf.sprintf "0x%02x" (Char.code c)) (List.map (fun c -> Printf.sprintf "0x%02x" (Char.code c))
(List.init (String.length s) (String.get s))) (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 = let string_const f s =
f.md.Emit.nstr <- f.md.Emit.nstr + 1;
let l = rodata_label f in let l = rodata_label f in
Buffer.add_string f.rodata (Printf.sprintf "\t.align 1\n%s:\n" l); Buffer.add_string f.rodata (Printf.sprintf "\t.align 1\n%s:\n" l);
if String.length s > 0 then 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 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. *) -- this file's idiom for a case it has not earned the right to compile. *)
let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) 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 if not dev then
unsupported unsupported
"x86 redefinition without cells: there is nothing to publish a body \ "x86 redefinition without cells: there is nothing to publish a body \
into, and this backend's release build has no indirection"; into, and this backend's release build has no indirection";
(match call with (* A thunk this module runs itself is excluded from all of the machinery
| Some _ -> below: [flan_reload_call] calls it directly, so it needs no cell, must not
unsupported be published into one, and must not take a registry slot -- there are 4096
"x86 redefinition: the transient flan_reload_call thunk is not built yet" of those and an expression evaluated in a loop would exhaust them.
| None -> ()); Nothing pointing into the module is also what lets the agent unload it. *)
let transient n = call = Some n in
let target name = let target name =
match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = name) p.Tast.fns with match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = name) p.Tast.fns with
| Some f -> f | 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 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 storage's, so the two are the same eight bytes and differ only in what
fills them. *) 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 = and new_globals =
List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) List.filter (fun (g : Tast.global) -> not (known g.Tast.gname))
p.Tast.globals p.Tast.globals
@ -3565,13 +3578,17 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
pc-relative [lea]. *) pc-relative [lea]. *)
List.iter List.iter
(fun (fn : Tast.fn) -> (fun (fn : Tast.fn) ->
if known fn.Tast.name then if transient fn.Tast.name then ()
load_int f.b ~dst:rax ~mm:(Got (csym fn.Tast.name)) ~size:8 ~signed:false else begin
else if known fn.Tast.name then
load_int f.b ~dst:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8 load_int f.b ~dst:rax ~mm:(Got (csym fn.Tast.name)) ~size:8
~signed:false; ~signed:false
lea f.b ~dst:r11 ~mm:(Sym (fsym fn.Tast.name, 0)); else
store_int f.b ~src:r11 ~mm:(Reg (rax, 0)) ~size:8) 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; targets;
(* Nothing a constant initialiser can do transfers, so this exit is (* Nothing a constant initialiser can do transfers, so this exit is
unreachable and is emitted only when something claims to aim at it. *) 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_buffer text f.b.out;
Buffer.add_string text Buffer.add_string text
"\t.size\tflan_reload_install, . - flan_reload_install\n\n"; "\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; Buffer.add_buffer rodata f.rodata;
let out = Buffer.create 8192 in let out = Buffer.create 8192 in
Buffer.add_buffer out text; Buffer.add_buffer out text;
@ -3640,6 +3689,34 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
Buffer.add_string out Buffer.add_string out
(Printf.sprintf "\n\t.data\n\t.align\t8\n%s:\n\t.quad\t%s\n" (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)); (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_string out "\n\t.section\t.rodata\n";
Buffer.add_buffer out rodata; Buffer.add_buffer out rodata;
Buffer.add_string out "\n\t.section\t.note.GNU-stack,\"\",@progbits\n"; Buffer.add_string out "\n\t.section\t.note.GNU-stack,\"\",@progbits\n";