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.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";