An --x86 host reloads an --x86 module, checked by running it

This commit is contained in:
Joseph Ferano 2026-09-13 22:40:52 +07:00
parent 69ea66deca
commit bd69f684ed
2 changed files with 73 additions and 5 deletions

View File

@ -611,9 +611,8 @@ let addr_into f ~reg (l : loc) =
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 three spellings of "name a symbol", each picking the pc-relative form
(* 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_mem f s = if f.ext s then Got s else Sym (s, 0)
let sym_loc f s = 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
@ -622,6 +621,19 @@ let addr_sym f ~dst s =
if f.ext s then load_int f.b ~dst ~mm:(Got s) ~size:8 ~signed:false
else lea f.b ~dst ~mm:(Sym (s, 0))
(* Read what a global holds, as opposed to where it is -- an indirection cell
is the only caller. Out of the GOT that is two loads, not one: the slot
holds the cell's *address*. Collapsing them was this lane's one real bug,
and it looked exactly right in the disassembly: [mov r11, cell@GOTPCREL(%rip)]
beside [call *%r11] reads as "call through the cell" and in fact calls the
cell. 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;
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)
@ -1005,7 +1017,7 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
address is in a slot there is nothing left to re-resolve. *)
| Tast.FnAddr (Tast.Fnval n) ->
if f.md.Emit.dev then
load_int f.b ~dst:rax ~mm:(sym_mem f (csym n)) ~size:8 ~signed:false
load_sym f ~dst:rax (csym n)
else addr_sym f ~dst:rax (fsym n);
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8
| Tast.FnAddr (Tast.Rtfn n) ->
@ -1816,7 +1828,7 @@ and call_flan f ~target ~args ~rty dst =
(match callee with
| `Sym s -> call_sym f.b s
| `Cell s ->
load_int f.b ~dst:r11 ~mm:(sym_mem f s) ~size:8 ~signed:false;
load_sym f ~dst:r11 s;
call_r f.b r11
| `Loc o ->
load_int f.b ~dst:r11 ~mm:(Frame o) ~size:8 ~signed:false;
@ -2607,7 +2619,11 @@ let program ~checks ?(dev = false) (p : Tast.program) : string =
Buffer.add_string rodata gr;
(match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns with
| Some fn -> Buffer.add_string text (emit_main md fn)
| None -> unsupported "no main");
(* No [main] is not an error, and [emit.ml] treats it the same way: a
program can be linked against a C host that brings its own entry point,
which is what [reload_host.c] is. Refusing here made a --x86 host for the
reload tests impossible to build. *)
| None -> ());
let out = Buffer.create 65536 in
Buffer.add_buffer out text;
(* The globals' initialiser runs before main, through the same constructor

View File

@ -200,6 +200,58 @@ let () =
if code <> 0 || text <> want then
fail "reload\n got: %S (exit %d)\n wanted: %S" text code want;
(* The same thing again, compiled by the dev backend end to end (x86.ml's
header, HANDOFF-x86-rt.md item 1). Both halves, host and module, because
the two backends' conventions agree on every scalar and disagree on
every aggregate: an LLVM-built module dlopened into an --x86 host would
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.
Read by running, not by reading. A disassembly reads correctly beside a
wrong answer often enough (DISCUSS.md item 15) that only the printed
transcript settles it: [outer] is compiled once into the host and never
rebuilt, so "after2 1204" can only mean its call site followed a body
that this backend emitted, published through a cell it reached via the
GOT. *)
let x86 = { dev with Build.x86 = true } in
let xhost = tmp "xhost" in
ignore
(Build.executable ~opts:x86 ~csrcs:[ "reload_host.c" ]
~lflags:[ "-ldl" ] p1 ~out:xhost);
let xmodule q fns name =
let o = tmp name in
let asm = X86.redefinition ~checks:true ~dev:true ~known q ~fns in
ignore (Build.shared_x86 ~opts:x86 ~asm ~out:o ());
o
in
let xso1 = xmodule p1 [ "bump" ] "xv1.so" in
let xso2 = xmodule p2 [ "bump" ] "xv2.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)
(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
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 _ -> ());
(* The layout-drift guard, which needs a process of its own because what it
does is abort one. [extra] does not exist in the host: v3 introduced it
at run time, so flan_dev.c allocated its storage and recorded its size,