diff --git a/lib/x86.ml b/lib/x86.ml index e17fc18..779c1e8 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -557,6 +557,17 @@ type fnctx = { module answers true for the host's cells, globals and bodies, and those go through the GOT -- see [modrm_got]. *) ext : string -> bool; + (* [Some slot] of a symbol that does not exist anywhere: a function or a + global the host was never built with, introduced by a redefinition module + while the process was running. There is no symbol to bind to and ELF has + no way to grow one, so the address is looked up by string at install time + and parked in [slot], which is this module's own object. Always [None] for + a whole program, where [known] is true of everything -- so the + whole-program path emits byte-identical output and the survey goes on + being a structural check on all of this. [ext] and this are two questions + and not one: [ext] says "the host's, reach it through the GOT", this says + "nobody's yet, reach it through a slot I filled". *) + slot : string -> string option; (* The line table under construction, in a [--debug] build. [None] is every other build, and then [dwline] below is the only thing that looks at it and does nothing — so a release build's output is byte-identical to what @@ -716,6 +727,13 @@ type loc = | Lf of int (* rbp + d *) | Lg of string * int (* rip-relative symbol + d *) | Lgot of string * int (* a symbol this object does not define; + d *) + (* A name that did not exist when the host was built, so there is no symbol + anywhere to bind to. The address lives in a slot this module defines and + [flan_reload_install] fills by asking the runtime's registry for it by + string. The slot is this object's own, so naming it is pc-relative and + never goes through the GOT; reading it is the one extra load, which is + exactly [Lgot]'s shape with [Sym] where it has [Got]. *) + | Lslot of string * int (* a module-local slot holds the address; + d *) | Lp of int * int (* [rbp + p] is a pointer; + d *) let shift l d = @@ -723,6 +741,7 @@ let shift l d = | Lf o -> Lf (o + d) | Lg (s, a) -> Lg (s, a + d) | Lgot (s, a) -> Lgot (s, a + d) + | Lslot (s, a) -> Lslot (s, a + d) | Lp (p, a) -> Lp (p, a + d) (* [scratch] is only touched by the [Lp] case, and every caller passes r11 — @@ -737,6 +756,9 @@ let lmem f (l : loc) ~scratch : mem = | Lgot (s, a) -> load_int f.b ~dst:scratch ~mm:(Got s) ~size:8 ~signed:false; Reg (scratch, a) + | Lslot (s, a) -> + load_int f.b ~dst:scratch ~mm:(Sym (s, 0)) ~size:8 ~signed:false; + Reg (scratch, a) | Lp (p, a) -> load_int f.b ~dst:scratch ~mm:(Frame p) ~size:8 ~signed:false; Reg (scratch, a) @@ -748,13 +770,19 @@ let addr_into f ~reg (l : loc) = | Lgot (s, a) -> load_int f.b ~dst:reg ~mm:(Got s) ~size:8 ~signed:false; if a <> 0 then add_imm f.b ~dst:reg a + | Lslot (s, a) -> + load_int f.b ~dst:reg ~mm:(Sym (s, 0)) ~size:8 ~signed:false; + if a <> 0 then add_imm f.b ~dst:reg a | Lp (p, a) -> load_int f.b ~dst:reg ~mm:(Frame p) ~size:8 ~signed:false; if a <> 0 then add_imm f.b ~dst:reg a (* The spellings of "name a symbol", each picking the pc-relative form for a symbol this object defines and the GOT form for one it does not. *) -let sym_loc f s = if f.ext s then Lgot (s, 0) else Lg (s, 0) +let sym_loc f s = + match f.slot s with + | Some sl -> Lslot (sl, 0) + | None -> if f.ext s then Lgot (s, 0) else Lg (s, 0) (* [lea] of a symbol is an address; out of the GOT the address is already there, so the [lea] becomes a load. *) @@ -769,11 +797,20 @@ let addr_sym f ~dst s = beside [call *%r11] reads as "call through the cell" and in fact calls the cell. docs/DISCUSS.md item 15 said this is how hand-encoding fails. *) let load_sym f ~dst s = - if f.ext s then begin - load_int f.b ~dst ~mm:(Got s) ~size:8 ~signed:false; + match f.slot s with + (* The slot holds the cell's address, just as the GOT entry below does, so + this is the same two loads with one relocation swapped. Which is the point + of spelling a run-time-new name this way: every site that reaches a cell + already double-loads, and none of them had to learn a third case. *) + | Some sl -> + load_int f.b ~dst ~mm:(Sym (sl, 0)) ~size:8 ~signed:false; load_int f.b ~dst ~mm:(Reg (dst, 0)) ~size:8 ~signed:false - end - else load_int f.b ~dst ~mm:(Sym (s, 0)) ~size:8 ~signed:false + | None -> + if f.ext s then begin + load_int f.b ~dst ~mm:(Got s) ~size:8 ~signed:false; + load_int f.b ~dst ~mm:(Reg (dst, 0)) ~size:8 ~signed:false + end + else load_int f.b ~dst ~mm:(Sym (s, 0)) ~size:8 ~signed:false let scalar_size f (t : Types.t) = match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t) @@ -2542,7 +2579,8 @@ let incoming_of ~sret (params : Types.t list) = sret_at, ps, next_int () let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false) - ?(hidden = false) ?dw (fn : Tast.fn) : string * string = + ?(slot = fun _ -> None) ?(hidden = false) ?dw (fn : Tast.fn) + : string * string = let b = create () in let nslots = Array.length fn.Tast.slots in let f = @@ -2551,7 +2589,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false) xfer_off = 0; sret_off = 0; retval = 0; frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = []; xfer_lbl = ""; unwound = false; - rodata = Buffer.create 64; externs; fns; ext; dw } + rodata = Buffer.create 64; externs; fns; ext; slot; dw } in (* The subprogram this function's rows hang off. Its first row is the function symbol itself, at the line the [defn] was written on, so the @@ -2874,7 +2912,7 @@ let emit_globals_init ?(cfi = false) (md : Emit.m) ~externs ~fns frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = []; xfer_lbl = ""; unwound = false; rodata = Buffer.create 64; externs; fns; ext = (fun _ -> false); - dw = None } + slot = (fun _ -> None); dw = None } in (* Two slots, not one: [xfer_off] holds the *pointer* every call passes on, and [cell] is what it points at. Storing a null into [xfer_off] itself — @@ -3350,21 +3388,34 @@ let program ~checks ?(dev = false) ?(debug = false) (p : Tast.program) : string frame boundary, on the game thread. [reload_host.c] and [vendor/agent/flan_agent.c] both [dlsym] exactly that spelling. - {b The scope, and it is narrower than [Emit.redefinition]'s.} Only names the - host already has. A name introduced since has no symbol to bind to, and - [Emit.redefinition] answers that with [flan_dev_cell] / [flan_dev_global] and - [Emit.cellptr]'s deeper spelling -- a module-local slot resolved by string at - install time. That is not built here, and neither is the [consts] republish - nor the transient [flan_reload_call] thunk. Each is refused by name, which is - this file's idiom for a case it has not earned the right to compile. *) + {b A name the host was never built with.} There is no symbol to bind to and + ELF has no way to grow one, so the address is asked for by string at install + time -- [flan_dev_cell] for a function's cell, [flan_dev_global] for a + global's storage -- and parked in a slot this module defines. [Lslot] is how + every later reference reads it, and it is [Lgot]'s shape with the relocation + swapped, so no call site and no place expression had to learn a third case. + [Emit.cellptr] and [Emit.globalptr] are the same two slots on the LLVM side, + spelled the same way here so the two are readable against each other. + + A new global's declared initial value travels with it, because + [flan_dev_global] copies it onto the allocation the first time the name is + seen and ignores it afterwards -- which is where "a reload must not reset the + program's state" lives. [emit.ml] folds that value into an LLVM constant; it + has a folder for the IR's own syntax and this file does not. So the image is + built the way [emit_globals_init] builds a global's storage in a whole + program: a module-local buffer, written by the initialiser expression lowered + as ordinary code. A few instructions once, and no second evaluator that could + disagree with the first about what a struct literal means. + + {b The scope, and it is still narrower than [Emit.redefinition]'s.} The + 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 = 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"; - if consts <> [] then - unsupported "x86 redefinition: republishing a defconst is not built yet"; (match call with | Some _ -> unsupported @@ -3390,24 +3441,15 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) let siblings = List.filter (fun (f : Tast.fn) -> f.Tast.fparent = None) p.Tast.fns in - List.iter - (fun (f : Tast.fn) -> - if not (known f.Tast.name) then - unsupported - "x86 redefinition: %s is new to this session, and a name the host \ - was not built with needs the flan_dev_cell lookup and \ - Emit.cellptr's second spelling, which are not built here yet" - f.Tast.name) - siblings; - List.iter - (fun (g : Tast.global) -> - if not (known g.Tast.gname) then - unsupported - "x86 redefinition: the global %s is new to this session, and a new \ - global needs the flan_dev_global lookup, which is not built here \ - yet" - g.Tast.gname) - p.Tast.globals; + (* The names this module introduces, each of which gets a slot below. A + 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 + and new_globals = + List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) + p.Tast.globals + in let md = layout_ctx ~checks ~dev p in let externs = Hashtbl.create 16 in List.iter @@ -3424,6 +3466,19 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) List.iter (fun (f : Tast.fn) -> Hashtbl.replace mine (fsym f.Tast.name) ()) (targets @ lifted); let ext s = not (Hashtbl.mem mine s) in + (* And the third answer: a name that is nobody's symbol yet. Keyed by the + spelling the reference uses -- a function is reached through its cell, so + the key is [csym]; a global is reached by its own name, so it is [gsym]. + The two can never collide, because a function and a global cannot share a + name and [gsym] and [fsym] are the same string. *) + let cellp n = asm_sym ("flan.cellp." ^ n) + and gp n = asm_sym ("flan.gp." ^ n) in + let slots = Hashtbl.create 8 in + List.iter (fun (f : Tast.fn) -> + Hashtbl.replace slots (csym f.Tast.name) (cellp f.Tast.name)) new_fns; + List.iter (fun (g : Tast.global) -> + Hashtbl.replace slots (gsym g.Tast.gname) (gp g.Tast.gname)) new_globals; + let slot s = Hashtbl.find_opt slots s in let text = Buffer.create 8192 and rodata = Buffer.create 1024 in Buffer.add_string text "# Generated by flan's x86-64 backend: one or more functions, recompiled\n\ @@ -3432,32 +3487,147 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true) \t.text\n\n"; List.iter (fun (f : Tast.fn) -> - let t, r = emit_fn md ~externs ~fns:fnstbl ~ext ~hidden:true f in + let t, r = emit_fn md ~externs ~fns:fnstbl ~ext ~slot ~hidden:true f in Buffer.add_string text t; Buffer.add_string rodata r) (lifted @ targets); - (* Publishing: one store per target, and the cell's address has to be read - out of the GOT first because the cell itself lives in the host. The body - is this module's own and hidden, so its address is an ordinary - pc-relative [lea]. *) - let b = create () in + (* [flan_reload_install] is a function with a frame, not a run of loads and + stores, and it has to be: it calls into the runtime, and a [call] made on + an unaligned stack faults inside glibc's own [movaps] rather than anywhere + a reader would look. A prologue is what makes rsp 16-aligned at every call + below, since [frame_bytes] rounds. Its shape is [emit_globals_init]'s and + for the same reasons, down to owning a null transfer cell that no caller + hands it. *) + let ib = create () in + let f = + { b = ib; md; fnname = ""; retlbl = new_label () "install"; + fret = Types.Unit; slots = [||]; xfer_off = 0; sret_off = 0; retval = 0; + frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = []; + xfer_lbl = ""; unwound = false; + rodata = Buffer.create 256; externs; fns = fnstbl; ext; slot; dw = None } + in + let chan = ptmp f in + f.xfer_off <- ptmp f; + f.xfer_lbl <- new_label f "ixfer"; + (* A new global's initial value, written into a buffer of this module's own. + [flan_dev_global] copies it the first time the name is interned and + ignores it on every call after, so a second module mentioning the same + name cannot reset the state the reload exists to preserve. Doing it before + any lookup is deliberate: nothing outside this module can see these + buffers, so they are not a publication and the order below is still + "resolve everything, then publish". *) + let images = + List.map + (fun (g : Tast.global) -> + let size, align = Emit.lay md g.Tast.gty in + let l = rodata_label f in + scoped f (fun () -> lower f g.Tast.ginit (Lg (l, 0))); + (g, l, max 1 size, max 1 align)) + new_globals + in + (* The lookups, all of them, before a single body is published. Publishing + first would expose a function whose slots are still null to anything that + called it, and here that means every call site in the host. [emit.ml] says + the same and [test_reload.ml] checks it there by grepping the IR text; + there is no text to grep on this side, so the guarantee is this loop + order and this comment. *) + let cstr sym = let l = string_const f sym in lea f.b ~dst:rdi ~mm:(Sym (l, 0)) in List.iter - (fun (f : Tast.fn) -> - load_int b ~dst:rax ~mm:(Got (csym f.Tast.name)) ~size:8 ~signed:false; - lea b ~dst:r11 ~mm:(Sym (fsym f.Tast.name, 0)); - store_int b ~src:r11 ~mm:(Reg (rax, 0)) ~size:8) + (fun (fn : Tast.fn) -> + cstr ("flan." ^ fn.Tast.name); + xor_rr f.b ~dst:rax ~src:rax; + call_sym f.b "flan_dev_cell"; + store_int f.b ~src:rax ~mm:(Sym (cellp fn.Tast.name, 0)) ~size:8) + new_fns; + List.iter + (fun ((g : Tast.global), l, size, _) -> + cstr ("flan." ^ g.Tast.gname); + movabs f.b ~dst:rsi (Int64.of_int size); + lea f.b ~dst:rdx ~mm:(Sym (l, 0)); + xor_rr f.b ~dst:rax ~src:rax; + call_sym f.b "flan_dev_global"; + store_int f.b ~src:rax ~mm:(Sym (gp g.Tast.gname, 0)) ~size:8) + images; + (* A [defconst] whose value the checker never folded is just bytes in the + program's memory, so a new value is published the same way a new body is: + one store, at the frame boundary the agent chose. One the checker did fold + is in the shape of the program and never gets here -- the session refuses + it before it asks for a module. *) + List.iter + (fun (g : Tast.global) -> + if List.exists (String.equal g.Tast.gname) consts then + scoped f (fun () -> lower f g.Tast.ginit (sym_loc f (gsym g.Tast.gname)))) + p.Tast.globals; + (* And now the bodies. A name the host has is published into its cell, whose + address comes out of the GOT because the cell lives in the host; a name it + does not is published through the slot the lookup above filled. Either way + the body is this module's own and hidden, so its address is an ordinary + 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) targets; - ret b; - flush b; + (* Nothing a constant initialiser can do transfers, so this exit is + unreachable and is emitted only when something claims to aim at it. *) + if f.unwound then begin + jmp_lbl f.b f.retlbl; lbl f.b f.xfer_lbl; jmp_lbl f.b f.retlbl + end; + let pb = create () in + push_r pb rbp; + mov_rr pb ~dst:rbp ~src:rsp; + let n = frame_bytes f in + if n > 0 then sub_imm pb ~dst:rsp n; + xor_rr pb ~dst:rax ~src:rax; + store_int pb ~src:rax ~mm:(Frame chan) ~size:8; + lea pb ~dst:rax ~mm:(Frame chan); + store_int pb ~src:rax ~mm:(Frame f.xfer_off) ~size:8; + lbl f.b f.retlbl; + leave f.b; + ret f.b; + flush pb; + flush f.b; Buffer.add_string text "\t.globl\tflan_reload_install\n\ \t.type\tflan_reload_install, @function\n\ flan_reload_install:\n"; - Buffer.add_buffer text b.out; + Buffer.add_buffer text pb.out; + Buffer.add_buffer text f.b.out; Buffer.add_string text "\t.size\tflan_reload_install, . - flan_reload_install\n\n"; + Buffer.add_buffer rodata f.rodata; let out = Buffer.create 8192 in Buffer.add_buffer out text; + (* The slots, and the buffers holding a new global's initial value. Both are + this module's own and neither is [.globl]: a second module introducing the + same name gets its own slot and fills it from the registry with the same + answer, which is exactly what makes two modules agree about a name that + has no symbol. [.bss], because every one of them is written before it is + read -- the slots by the lookups above, the images by the initialisers. *) + if new_fns <> [] || new_globals <> [] then begin + Buffer.add_string out "\n\t.bss\n"; + List.iter + (fun (fn : Tast.fn) -> + Buffer.add_string out + (Printf.sprintf "\t.align\t8\n\t.type\t%s, @object\n\ + \t.size\t%s, 8\n%s:\n\t.zero\t8\n" + (cellp fn.Tast.name) (cellp fn.Tast.name) (cellp fn.Tast.name))) + new_fns; + List.iter + (fun ((g : Tast.global), l, size, align) -> + let s = gp g.Tast.gname in + Buffer.add_string out + (Printf.sprintf "\t.align\t8\n\t.type\t%s, @object\n\ + \t.size\t%s, 8\n%s:\n\t.zero\t8\n\ + \t.align\t%d\n%s:\n\t.zero\t%d\n" + s s s align l size)) + images + end; (* The ABI marker this module requires of its host. A pointer-sized datum holding the host's marker is a relocation the loader has to resolve while it maps the object, whatever it does about lazy binding of calls, so a diff --git a/test/programs/reload-v6.flan b/test/programs/reload-v6.flan new file mode 100644 index 0000000..4065cc8 --- /dev/null +++ b/test/programs/reload-v6.flan @@ -0,0 +1,17 @@ +;;;; A run-time-new global with a value of its own, which is the half of the +;;;; new-name path that a zeroed defvar cannot measure: [flan_dev_global] +;;;; allocates with calloc, so a global declared zero looks right whether or +;;;; not its initial value travelled with the module. [tuning] is 42, and the +;;;; transcript is the number the host prints, so an image that never arrived +;;;; prints 4 rather than 88. +(defvar counter i64) +(defvar tuning i64 42) + +(defn helper [x i64] i64 (* x 2)) + +(defn bump [] i64 + (println "v6") + (set counter (+ counter tuning 1)) + (helper counter)) + +(defn outer [] i64 (bump)) diff --git a/test/test_reload.ml b/test/test_reload.ml index 43b4215..ff23ce7 100644 --- a/test/test_reload.ml +++ b/test/test_reload.ml @@ -207,10 +207,12 @@ let () = be correct until the first redefined function took or returned a struct. So an --x86 host gets --x86 modules and the two never meet. - Only v1 and v2, which is the whole of what X86.redefinition compiles: - every name they touch is one the host was built with. v3 and v4 - introduce a function and a global at run time, and the flan_dev_cell / - flan_dev_global lookups that needs are refused there by name. + All four modules, and the same expected string as the LLVM path above: + v3 introduces a defvar and a defn the host was never built with, and v4 + redefines the one v3 introduced. Neither has a symbol anywhere, so both + go through flan_dev.c's by-name registry into a slot the module defines + and [flan_reload_install] fills -- [X86.Lslot], which is the GOT path + with the relocation swapped. Read by running, not by reading. A disassembly reads correctly beside a wrong answer often enough (docs/DISCUSS.md item 15) that only the printed @@ -231,26 +233,46 @@ let () = in let xso1 = xmodule p1 [ "bump" ] "xv1.so" in let xso2 = xmodule p2 [ "bump" ] "xv2.so" in + let xso3 = xmodule p3 [ "bump"; "added" ] "xv3.so" in + let xso4 = xmodule p4 [ "added" ] "xv4.so" in let xout = tmp "xout" in let xcode = Sys.command - (Printf.sprintf "%s %s %s > %s 2> %s" (Filename.quote xhost) - (Filename.quote xso1) (Filename.quote xso2) (Filename.quote xout) + (Printf.sprintf "%s %s %s %s %s > %s 2> %s" (Filename.quote xhost) + (Filename.quote xso1) (Filename.quote xso2) (Filename.quote xso3) + (Filename.quote xso4) (Filename.quote xout) (Filename.quote (tmp "xerr"))) in let xtext = In_channel.with_open_bin xout In_channel.input_all in - let xwant = - "v1\nhost 2\nv1\nafter1 4\n" ^ v2s ^ "after2 1204\ncounter 102\n" - in - if xcode <> 0 || xtext <> xwant then + if xcode <> 0 || xtext <> want then fail "x86 reload\n got: %S (exit %d)\n wanted: %S" xtext xcode - xwant; - (* A name the host was never built with has no symbol to bind to, and the - registry path is not built here. It has to refuse rather than emit - something that links and then stores through a null. *) - (match X86.redefinition ~checks:true ~dev:true ~known p3 ~fns:[ "added" ] with - | _ -> fail "x86 redefinition accepted a name the host does not have" - | exception X86.Unsupported _ -> ()); + want; + (* [extra] is a defvar the host has no storage for, so its declared value + has to travel with it: [flan_dev_global] copies the module's image onto + the allocation the first time the name is interned and ignores it every + time after. [extra] is declared zero here, which calloc would also give, + so the case is asserted where it is visible -- a run-time-new global + with a value of its own. *) + let p6 = checked "programs/reload-v6.flan" in + let xso6 = xmodule p6 [ "bump" ] "xv6.so" in + let xhost6 = tmp "xhost6" in + ignore + (Build.executable ~opts:x86 ~csrcs:[ "reload_host.c" ] + ~lflags:[ "-ldl" ] p1 ~out:xhost6); + let xout6 = tmp "xout6" in + let xcode6 = + Sys.command + (Printf.sprintf "%s %s > %s 2> %s" (Filename.quote xhost6) + (Filename.quote xso6) (Filename.quote xout6) + (Filename.quote (tmp "xerr6"))) + in + let xtext6 = In_channel.with_open_bin xout6 In_channel.input_all in + let xwant6 = "v1\nhost 2\nv6\nafter1 88\ncounter 44\n" in + if xcode6 <> 0 || xtext6 <> xwant6 then + fail "x86 reload of a new global with a value\n \ + got: %S (exit %d)\n wanted: %S" xtext6 xcode6 xwant6; + List.iter (fun p -> try Sys.remove p with Sys_error _ -> ()) + [ xso1; xso2; xso3; xso4; xso6; xhost; xhost6; xout; xout6 ]; (* The aggregate case, which is the whole reason X86.redefinition exists rather than an --x86 host dlopening what Emit.redefinition made.