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