Merge: an x86 dev build pushes the frames the inspector reads
This commit is contained in:
commit
6055e9c247
205
lib/x86.ml
205
lib/x86.ml
@ -448,8 +448,18 @@ let layout_ctx ~checks ~dev (p : Tast.program) : Emit.m =
|
||||
p.Tast.datas;
|
||||
List.iter (fun (u : Tast.structure) -> Hashtbl.replace unions u.Tast.sname u)
|
||||
p.Tast.unions;
|
||||
(* The globals table is filled and not left empty, which it was for as long
|
||||
as nothing here asked a question about a name. [Reach.ref_fingerprint]
|
||||
does: it hashes the set of *globals* a body names, and with an empty table
|
||||
no name is a global, so every frame descriptor would carry the hash of the
|
||||
empty set while the daemon recomputes it against the real one. The frames
|
||||
would then all read as superseded and the break loop would refuse the
|
||||
locals of a body nobody had touched. *)
|
||||
let globals = Hashtbl.create 16 in
|
||||
List.iter (fun (g : Tast.global) -> Hashtbl.replace globals g.Tast.gname g.Tast.gty)
|
||||
p.Tast.globals;
|
||||
{ Emit.out = Buffer.create 1; strs = Buffer.create 1; structs; datas; unions;
|
||||
globals = Hashtbl.create 1; externs = Hashtbl.create 1; checks;
|
||||
globals; externs = Hashtbl.create 1; checks;
|
||||
dev; known = (fun _ -> true); dbg = None; sanitize = false;
|
||||
nstr = 0; nfi = 0 }
|
||||
|
||||
@ -616,6 +626,22 @@ type fnctx = {
|
||||
mutable xfer_off : int; (* the incoming transfer channel pointer *)
|
||||
mutable sret_off : int; (* where the hidden return pointer was put *)
|
||||
mutable retval : int; (* the scalar return value's temporary *)
|
||||
(* The shadow-stack record this function pushes, in a dev build: three words
|
||||
at [rbp+dframe], holding the previous head, the static description, and
|
||||
the slot table. [None] in a release build, where there is no record and
|
||||
the epilogue below restores nothing. *)
|
||||
mutable dframe : int option;
|
||||
(* Where a dev build records each named slot's address, so that a stopped
|
||||
frame's locals can be read: one word per Tast slot at [rbp+dslotv]. [None]
|
||||
in a release build and in a function with no named slot at all — the same
|
||||
gate [emit.ml] applies, and for a reason that is LLVM's rather than ours
|
||||
(an alloca whose address escapes stops being promotable). Keeping the gate
|
||||
anyway is what makes the two backends' frames answer identically, which is
|
||||
the only thing the break loop can check. *)
|
||||
mutable dslotv : int option;
|
||||
(* [fn.snames], carried so [bind_slot] can ask whether a slot has a name to
|
||||
show without the whole [Tast.fn] being threaded to every binding site. *)
|
||||
snames : string option array;
|
||||
mutable frame : int; (* bytes currently allocated below rbp *)
|
||||
mutable maxframe : int;
|
||||
mutable outgoing : int; (* bytes the widest call needs for stack args *)
|
||||
@ -936,6 +962,71 @@ let float_const f (x : float) ~f64 =
|
||||
(Printf.sprintf "\t.align 4\n%s:\n\t.long 0x%lx\n" l (Int32.bits_of_float x));
|
||||
l
|
||||
|
||||
(* ── The shadow stack's descriptors ──────────────────────────────────── *)
|
||||
|
||||
(* One [flan_fninfo] (runtime/flan_dev.c) per function in a dev build: the
|
||||
name and the location as bytes and lengths, how many slots the frame has,
|
||||
and the two fingerprints the daemon compares a frame against. Static data —
|
||||
nothing about a function changes between two calls to it — so the record a
|
||||
call pushes is three words and points at this.
|
||||
|
||||
Not through [string_const], and that is the whole reason this has its own
|
||||
emitter rather than borrowing that one. [string_const] bumps [md.nstr],
|
||||
which is the test [redefinition] applies before it lets an expression
|
||||
thunk's module say nothing points into it: a literal in the image may be
|
||||
held by the program afterwards, so a module with one keeps its mapping. A
|
||||
descriptor is not held — the frames naming it were popped on the way out and
|
||||
the break loop copies the bytes it shows. Counting these there would stop
|
||||
every C-x C-e module from ever being unloaded, which is [emit.ml]'s [m.nfi]
|
||||
comment and the same trap on this side.
|
||||
|
||||
The bytes carry no NUL and are not meant to: every reader takes the length
|
||||
beside the pointer. *)
|
||||
let fi_bytes f s =
|
||||
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
|
||||
Buffer.add_string f.rodata (Printf.sprintf "\t.byte %s\n" (escape_bytes s));
|
||||
l, String.length s
|
||||
|
||||
(* [.data.rel.ro] and not [.rodata], which every other constant here goes in.
|
||||
This is the only constant this backend emits that holds an *address*, and an
|
||||
address in a shared object is a relocation the loader applies at load time —
|
||||
so the section has to be one the loader may write. A redefinition module is
|
||||
exactly such an object. The section is switched back afterwards because the
|
||||
caller's buffer is emitted inside [.rodata] and everything after this in it
|
||||
belongs there. *)
|
||||
let fninfo f (fn : Tast.fn) ~nslots =
|
||||
let nlbl, nlen = fi_bytes f fn.Tast.name in
|
||||
let llbl, llen = fi_bytes f (Loc.to_string fn.Tast.floc) in
|
||||
let l = rodata_label f in
|
||||
Buffer.add_string f.rodata
|
||||
(Printf.sprintf
|
||||
"\t.section\t.data.rel.ro,\"aw\"\n\t.align 8\n%s:\n\
|
||||
\t.quad\t%s\n\t.quad\t%d\n\t.quad\t%s\n\t.quad\t%d\n\
|
||||
\t.long\t%d\n\t.long\t%d\n\t.long\t%d\n\t.long\t0\n\
|
||||
\t.section\t.rodata\n"
|
||||
l nlbl nlen llbl llen nslots (Emit.slot_fingerprint fn)
|
||||
(Reach.ref_fingerprint ~is_global:(Hashtbl.mem f.md.Emit.globals) fn));
|
||||
l
|
||||
|
||||
(* The store that says "this slot is bound now", and it is the address rather
|
||||
than a flag for [emit.ml]'s reason: the reader needs the address anyway, so
|
||||
one store carries both facts, and a slot the control flow has not reached
|
||||
reads as null rather than as a plausible value at an address nobody wrote.
|
||||
|
||||
rax is free at every site that calls this — each one has just finished
|
||||
moving a value into the slot, and nothing in this backend is live in a
|
||||
register across a statement. *)
|
||||
let bind_slot f i =
|
||||
match f.dslotv with
|
||||
| None -> ()
|
||||
| Some sv ->
|
||||
if i < Array.length f.snames && f.snames.(i) <> None then begin
|
||||
lea f.b ~dst:rax ~mm:(Frame f.slots.(i));
|
||||
store_int f.b ~src:rax ~mm:(Frame (sv + (8 * i))) ~size:8
|
||||
end
|
||||
|
||||
(* ── Locations ───────────────────────────────────────────────────────── *)
|
||||
|
||||
(* Where a value lives. Every value in this backend lives in memory, so the
|
||||
@ -1476,7 +1567,8 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
| Tast.Let (bs, body) ->
|
||||
List.iter
|
||||
(fun (slot, (v : Tast.expr)) ->
|
||||
scoped f (fun () -> lower f v (Lf f.slots.(slot))))
|
||||
scoped f (fun () -> lower f v (Lf f.slots.(slot)));
|
||||
bind_slot f slot)
|
||||
bs;
|
||||
block f body dst t
|
||||
| Tast.If (c, a, b) ->
|
||||
@ -1847,7 +1939,8 @@ and emit_restart_case f clauses body dst t =
|
||||
List.iteri
|
||||
(fun i (slot_i, ty) ->
|
||||
move f ~dst:(Lf f.slots.(slot_i))
|
||||
~src:(Lp (bufp, List.nth offs i)) ty)
|
||||
~src:(Lp (bufp, List.nth offs i)) ty;
|
||||
bind_slot f slot_i)
|
||||
c.Tast.rparams);
|
||||
scoped f (fun () -> block f c.Tast.rbody dst t);
|
||||
jmp_lbl f.b ld;
|
||||
@ -2045,7 +2138,8 @@ and emit_match f (scrut : Tast.expr) (arms : Tast.arm list) dst t =
|
||||
let src, fty =
|
||||
bind_at (match a.Tast.acase with Some c -> c | None -> "") k
|
||||
in
|
||||
move f ~dst:(Lf f.slots.(slot)) ~src fty)
|
||||
move f ~dst:(Lf f.slots.(slot)) ~src fty;
|
||||
bind_slot f slot)
|
||||
a.Tast.binds;
|
||||
block f a.Tast.abody dst t;
|
||||
jmp_lbl f.b lend;
|
||||
@ -2935,7 +3029,8 @@ let where_from = function
|
||||
| Istk d -> Printf.sprintf "from the caller's stack, at rbp+0x%x" d
|
||||
|
||||
let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
|
||||
~xfer_off ~sret_off ~retval ~sret ~sret_at ~param_at ~xfer_at =
|
||||
~xfer_off ~sret_off ~retval ~dframe ~dslotv ~sret ~sret_at ~param_at
|
||||
~xfer_at =
|
||||
let b = Buffer.create 1024 in
|
||||
let line s = Buffer.add_string b (if s = "" then "#\n" else "# " ^ s ^ "\n") in
|
||||
(* The prose paragraphs wrap; the table below does not, because its columns
|
||||
@ -3053,6 +3148,18 @@ let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
|
||||
if (not sret) && not (is_void fn.Tast.ret) then
|
||||
row retval "<ret>" (Types.to_string fn.Tast.ret)
|
||||
"the return value the epilogue loads";
|
||||
(* Present in a dev build and absent from a release one, and the map says
|
||||
which build this is by saying nothing when there is nothing there. *)
|
||||
(match dframe with
|
||||
| Some off ->
|
||||
row off "<frame>" "ptr[3]"
|
||||
"the shadow-stack record: the previous head, the description, the slots"
|
||||
| None -> ());
|
||||
(match dslotv with
|
||||
| Some off ->
|
||||
row off "<slots>" "ptr[]"
|
||||
"one address per slot, null until the binding that fills it has run"
|
||||
| None -> ());
|
||||
para (Printf.sprintf
|
||||
"Everything below -0x%x is a temporary. They are bump-allocated and reclaimed \
|
||||
at the end of the form that made them, so a later form reuses the bytes and \
|
||||
@ -3069,6 +3176,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
{ b; md; fnname = fn.Tast.name; retlbl = "";
|
||||
fret = fn.Tast.ret; slots = Array.make nslots 0;
|
||||
xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; snames = fn.Tast.snames;
|
||||
frame = 0; maxframe = 0; outgoing = 0;
|
||||
loops = []; pads = []; xfer_lbl = ""; unwound = false;
|
||||
rodata = Buffer.create 64; externs; fns; ext; slot; dw;
|
||||
@ -3111,6 +3219,24 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
f.xfer_off <- ptmp f;
|
||||
if sret then f.sret_off <- ptmp f;
|
||||
if (not sret) && not (is_void fn.Tast.ret) then f.retval <- tmp f fn.Tast.ret;
|
||||
(* The shadow stack's storage, in a dev build and nowhere else: three words
|
||||
for the record and one per slot for the table. Allocated here, beside the
|
||||
channel and the return temporary, so that they sit above [fixed] and the
|
||||
frame map has somewhere to name them; and allocated outside every [scoped]
|
||||
so nothing below reuses the bytes.
|
||||
|
||||
Only a function with at least one *named* slot gets a table, which is the
|
||||
gate [emit.ml] applies. There it buys an optimisation — an alloca whose
|
||||
address escapes is one mem2reg cannot promote — and nothing here promotes
|
||||
anything, so on this side it buys only the stores. It is kept because the
|
||||
two backends have to answer a stopped frame identically: a function whose
|
||||
every slot the compiler invented reports no slots on LLVM, and a listing
|
||||
that differed by backend is the one thing the break loop cannot check. *)
|
||||
if md.Emit.dev then begin
|
||||
if nslots > 0 && Array.exists (fun n -> n <> None) fn.Tast.snames then
|
||||
f.dslotv <- Some (alloc f (8 * nslots) 8);
|
||||
f.dframe <- Some (alloc f 24 8)
|
||||
end;
|
||||
f.retlbl <- new_label f "ret";
|
||||
f.xfer_lbl <- new_label f "xfer";
|
||||
(* Where the named part of the frame ends and the nameless part begins. Read
|
||||
@ -3132,6 +3258,55 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
| _ -> None)
|
||||
fn.Tast.params param_at
|
||||
in
|
||||
(* The shadow stack's push, and the pop is in the epilogue. plan.org has had
|
||||
*Frames: shadow stack* in the dev column since the beginning; [emit.ml]
|
||||
builds the same four words on the LLVM side and this is the x86 one, down
|
||||
to which fields are written and when, so that a break loop cannot tell
|
||||
which backend it stopped.
|
||||
|
||||
Inline rather than a call, for [emit.ml]'s reason: this is on every call in
|
||||
a dev build, and a call made to record a call would be most of what it
|
||||
costs. What it does cost here is a handful of stores — the table's zeroing,
|
||||
three words of record, one head store, and one [lea]+store per named
|
||||
parameter.
|
||||
|
||||
Emitted into the body buffer rather than the prologue's, and that is not a
|
||||
detail: the prologue is where the incoming registers still hold the
|
||||
arguments, and every instruction below clobbers rax. The body buffer's
|
||||
first byte is the first point at which they are all safely in the frame.
|
||||
|
||||
One push per [emit_fn] and no more. [emit_main], [emit_globals_init] and
|
||||
[flan_reload_install] build their context by hand and push nothing, which
|
||||
is what [emit.ml] does too: they are C's frames, not the program's, and a
|
||||
backtrace that named them would be naming the runtime. *)
|
||||
(match f.dframe with
|
||||
| None -> ()
|
||||
| Some fr ->
|
||||
if ann then set_ind f.b "";
|
||||
note f
|
||||
"The shadow stack's push — runtime/flan_dev.c. Dev builds only, and it is what \
|
||||
lets a stopped program say where it is. The pop is the first thing in the \
|
||||
epilogue, so a transfer out of this frame pops it too.";
|
||||
(* Every entry, not only the named ones: "null means not bound" has to
|
||||
hold at every index, or a reader has to know which indices it may
|
||||
trust, and that is a second thing to keep in step. *)
|
||||
(match f.dslotv with
|
||||
| Some sv -> zero_frame f ~dst:sv (8 * nslots)
|
||||
| None -> ());
|
||||
let head = sym_loc f "flan_frame_head" in
|
||||
load_int f.b ~dst:rax ~mm:(lmem f head ~scratch:r11) ~size:8 ~signed:false;
|
||||
store_int f.b ~src:rax ~mm:(Frame fr) ~size:8;
|
||||
addr_into f ~reg:rax (Lg (fninfo f fn ~nslots:(if f.dslotv = None then 0 else nslots), 0));
|
||||
store_int f.b ~src:rax ~mm:(Frame (fr + 8)) ~size:8;
|
||||
(match f.dslotv with
|
||||
| Some sv -> lea f.b ~dst:rax ~mm:(Frame sv)
|
||||
| None -> xor_rr f.b ~dst:rax ~src:rax);
|
||||
store_int f.b ~src:rax ~mm:(Frame (fr + 16)) ~size:8;
|
||||
lea f.b ~dst:rax ~mm:(Frame fr);
|
||||
store_int f.b ~src:rax ~mm:(lmem f head ~scratch:r11) ~size:8;
|
||||
(* The parameters are bound before the body starts, so they are recorded
|
||||
here rather than at a binding site there is none of. *)
|
||||
List.iteri (fun i _ -> bind_slot f i) fn.Tast.params);
|
||||
(* The body. Lowered into its own buffer, because the prologue's [sub] needs
|
||||
a frame size only the body can decide, and every relocation this backend
|
||||
emits is an assembler expression — so nothing has to be patched. *)
|
||||
@ -3292,6 +3467,21 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
"The epilogue, and every return and every transfer out of this frame arrives here, \
|
||||
so the frame is torn down once.";
|
||||
lbl f.b f.retlbl;
|
||||
(* The shadow stack's pop, and it is one store because this backend has one
|
||||
epilogue: a [return], the fall through the body's tail, and the transfer
|
||||
exit all arrive at this label, so the head is restored on the unwinding
|
||||
path as well as on the normal one. [emit.ml] needs the same restore at
|
||||
five separate [ret]s and routes them through its [ret] for exactly that
|
||||
reason — a pop written only on the normal path leaves a dead frame behind
|
||||
every handled condition, and the next backtrace is a lie.
|
||||
|
||||
Before the return value is loaded, because it is loaded into rax. *)
|
||||
(match f.dframe with
|
||||
| None -> ()
|
||||
| Some fr ->
|
||||
load_int f.b ~dst:rax ~mm:(Frame fr) ~size:8 ~signed:false;
|
||||
store_int f.b ~src:rax
|
||||
~mm:(lmem f (sym_loc f "flan_frame_head") ~scratch:r11) ~size:8);
|
||||
if sret then load_int f.b ~dst:rax ~mm:(Frame f.sret_off) ~size:8 ~signed:false
|
||||
else if not (is_void fn.Tast.ret) then
|
||||
load_scalar f ~reg:(if is_float fn.Tast.ret then xmm0 else rax)
|
||||
@ -3307,7 +3497,8 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
Buffer.add_string out
|
||||
(frame_map md fn ~slots:f.slots ~fixed ~total:(frame_bytes f)
|
||||
~outgoing:f.outgoing ~xfer_off:f.xfer_off ~sret_off:f.sret_off
|
||||
~retval:f.retval ~sret ~sret_at ~param_at ~xfer_at);
|
||||
~retval:f.retval ~dframe:f.dframe ~dslotv:f.dslotv ~sret ~sret_at
|
||||
~param_at ~xfer_at);
|
||||
Buffer.add_string out (Printf.sprintf "\t.globl\t%s\n" sym);
|
||||
(* [emit.ml:2072] says this is load-bearing and it is: default visibility in
|
||||
a shared object is interposable, and that applies to taking the address
|
||||
@ -3454,6 +3645,7 @@ let emit_globals_init ?(cfi = false) ?(ann = false) ~sym (md : Emit.m) ~externs
|
||||
let f =
|
||||
{ b; md; fnname = "<globals>"; retlbl = new_label () "ginit";
|
||||
fret = Types.Unit; slots = [||]; xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; snames = [||];
|
||||
frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = [];
|
||||
xfer_lbl = ""; unwound = false;
|
||||
rodata = Buffer.create 64; externs; fns; ext = (fun _ -> false);
|
||||
@ -4096,6 +4288,7 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
let f =
|
||||
{ b = ib; md; fnname = "<install>"; retlbl = new_label () "install";
|
||||
fret = Types.Unit; slots = [||]; xfer_off = 0; sret_off = 0; retval = 0;
|
||||
dframe = None; dslotv = None; snames = [||];
|
||||
frame = 0; maxframe = 0; outgoing = 0; loops = []; pads = [];
|
||||
xfer_lbl = ""; unwound = false;
|
||||
rodata = Buffer.create 256; externs; fns = fnstbl; ext; slot; dw = None;
|
||||
|
||||
187
test/test_dev.ml
187
test/test_dev.ml
@ -4030,10 +4030,197 @@ let () =
|
||||
if status r <> "ok" then fail "x86 new name round trip: %s" (said r)
|
||||
else if value r <> "82" then
|
||||
fail "x86 (twice fresh) answered %S, so the registry lookups did not resolve" (value r);
|
||||
(* And a backtrace *through* a redefined body, which is the one frame a
|
||||
module has to get right on its own: the descriptor a body points at
|
||||
travels in the object that body was compiled into, so a module that
|
||||
pushed no frame would leave a gap where the redefinition ran, and one
|
||||
that pointed at the host's descriptor would report the location of the
|
||||
body it replaced. The installed body's own file is what says which
|
||||
happened. Last in this block deliberately — it stops the program. *)
|
||||
let r =
|
||||
request c
|
||||
"(:op \"eval\" :code \"(defn step [] i64 (let [n (+ ticks 1)] (error (Missing {.id 3})) n))\" :file \"/tmp/x86redef.flan\")"
|
||||
in
|
||||
if status r <> "ok" then fail "x86 redefined-body backtrace, install: %s" (said r)
|
||||
else if not (await (fun () ->
|
||||
match Wire.field (request c "(:op \"describe\")") "stopped" with
|
||||
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||
| _ -> false))
|
||||
then fail "the --x86 program never stopped in the redefined body"
|
||||
else begin
|
||||
let r = request c "(:op \"backtrace\")" in
|
||||
let top =
|
||||
match Wire.field r "frames" with
|
||||
| Some { Form.v = Form.List ({ Form.v = Form.List
|
||||
({ Form.v = Form.Str n; _ }
|
||||
:: { Form.v = Form.Str loc; _ } :: _); _ }
|
||||
:: _); _ } -> Some (n, loc)
|
||||
| _ -> None
|
||||
in
|
||||
match top with
|
||||
| Some ("step", loc) when contains_sub loc "x86redef.flan" -> ()
|
||||
| Some (n, loc) -> fail "x86 backtrace through a redefined body: %s at %S" n loc
|
||||
| None -> fail "x86 backtrace through a redefined body: %s" (said r)
|
||||
end;
|
||||
ignore (request c "(:op \"close\")");
|
||||
(try Unix.close c with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] xpid2) with Unix.Unix_error _ -> ())
|
||||
end;
|
||||
|
||||
(* ── The inspector, on the other backend ──────────────────────────── *)
|
||||
|
||||
(* The break loop's four questions asked of an [--x86] host. They are the
|
||||
ones that go through the shadow stack rather than through a module: the
|
||||
frame chain gives the depth and the names, and the addresses in it are
|
||||
what a render thunk reads the locals and the globals out of. Until
|
||||
[X86.emit_fn] pushed a frame this daemon answered every one of them with
|
||||
"this program was not built with --dev", which was false of it.
|
||||
|
||||
[programs/dev-locals.flan] and not a fixture of its own, deliberately:
|
||||
the LLVM block above asks these same questions of that same program, so
|
||||
the two sets of answers can be read against each other, and what is
|
||||
being claimed is that they are the *same* answers rather than merely
|
||||
plausible ones. A backend the break loop can tell apart is a backend the
|
||||
break loop cannot be trusted on. *)
|
||||
let isock = tmp "x86locals.sock" and iout = tmp "x86locals.out" in
|
||||
(try Sys.remove isock with Sys_error _ -> ());
|
||||
let ifd =
|
||||
Unix.openfile iout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||
in
|
||||
let ipid =
|
||||
Unix.create_process flan
|
||||
[| flan; "dev"; "programs/dev-locals.flan"; "-s"; isock; "--x86" |]
|
||||
Unix.stdin ifd Unix.stderr
|
||||
in
|
||||
Unix.close ifd;
|
||||
if not (listening ~pid:ipid isock) then begin
|
||||
fail "the --x86 inspector daemon %s (%S)" !listen_why
|
||||
(In_channel.with_open_bin iout In_channel.input_all);
|
||||
(try Unix.kill ipid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end
|
||||
else begin
|
||||
let c = connect isock in
|
||||
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
||||
let stopped r =
|
||||
match Wire.field r "stopped" with
|
||||
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||
| _ -> false
|
||||
in
|
||||
if not (await (fun () -> stopped (request c "(:op \"describe\")"))) then
|
||||
fail "the --x86 inspector program never stopped"
|
||||
else begin
|
||||
(* The backtrace first, because everything below names a frame by the
|
||||
index this listing gives. Two frames and not one: [main] called
|
||||
[look], so a chain with only the innermost on it would mean the push
|
||||
happened and the previous head was not saved. *)
|
||||
let r = request c "(:op \"backtrace\")" in
|
||||
let frames =
|
||||
match Wire.field r "frames" with
|
||||
| Some { Form.v = Form.List l; _ } ->
|
||||
List.filter_map
|
||||
(fun (e : Form.t) ->
|
||||
match e.Form.v with
|
||||
| Form.List ({ Form.v = Form.Str n; _ }
|
||||
:: { Form.v = Form.Str loc; _ }
|
||||
:: { Form.v = Form.Str origin; _ } :: _) ->
|
||||
Some (n, loc, origin)
|
||||
| _ -> None)
|
||||
l
|
||||
| _ -> []
|
||||
in
|
||||
if status r <> "ok" then fail "x86 backtrace: %s" (said r)
|
||||
else
|
||||
(match frames with
|
||||
| [ ("look", l0, "program"); ("main", _, "program") ] ->
|
||||
(* The location travels in the frame's own descriptor, so a wrong
|
||||
one is a descriptor built from the wrong function rather than a
|
||||
cosmetic slip. *)
|
||||
if not (contains_sub l0 "dev-locals.flan:14") then
|
||||
fail "x86 backtrace put look at %S" l0
|
||||
| _ ->
|
||||
fail "x86 backtrace: %s"
|
||||
(String.concat ", "
|
||||
(List.map (fun (n, _, o) -> n ^ "/" ^ o) frames)));
|
||||
let triples r key =
|
||||
match Wire.field r key with
|
||||
| Some { Form.v = Form.List l; _ } ->
|
||||
List.filter_map
|
||||
(fun (e : Form.t) ->
|
||||
match e.Form.v with
|
||||
| Form.List ({ Form.v = Form.Str a; _ }
|
||||
:: { Form.v = Form.Str b; _ } :: rest) ->
|
||||
Some (a, b,
|
||||
match rest with
|
||||
| { Form.v = Form.Str c; _ } :: _ -> c
|
||||
| _ -> "")
|
||||
| _ -> None)
|
||||
l
|
||||
| _ -> []
|
||||
in
|
||||
(* The locals of the stopped frame, and this is the claim the slot
|
||||
table exists for: the values are the ones [look] was called with and
|
||||
bound to, read at the addresses the frame recorded, under this
|
||||
backend's own frame layout. A wrong address renders whatever those
|
||||
bytes happen to be, so matching the LLVM listing exactly is the
|
||||
check and a shape-only assertion would not be. *)
|
||||
let r = request c "(:op \"locals\" :frame 0)" in
|
||||
if status r <> "ok" then fail "x86 locals: %s" (said r)
|
||||
else begin
|
||||
let want =
|
||||
[ ("n", "i64", "3");
|
||||
("label", "string", "\"hello\"");
|
||||
("p", "Point", "(Point {.x 1.5 .y 2.5})");
|
||||
("xs", "[3 i32]", "[ 10 20 30]");
|
||||
("flag", "bool", "true") ]
|
||||
in
|
||||
let got = triples r "locals" in
|
||||
if got <> want then
|
||||
fail "x86 locals of the stopped frame: %s"
|
||||
(String.concat ", "
|
||||
(List.map (fun (n, ty, v) -> n ^ " " ^ ty ^ " = " ^ v) got));
|
||||
(* [after] is bound past the error, so nothing wrote its entry and it
|
||||
is still the null the push stored. That null is the whole of how
|
||||
"not bound yet" is told from "bound" on this side too — there is no
|
||||
liveness analysis behind it — so a frame whose table were left
|
||||
uninitialised would render this one from stack litter. *)
|
||||
match List.filter (fun (n, _, _) -> n = "after") (triples r "refused") with
|
||||
| [ (_, why, _) ] when why <> "" -> ()
|
||||
| _ ->
|
||||
fail "x86: a slot bound after the error was not refused by name: %s"
|
||||
(String.concat ", "
|
||||
(List.map (fun (n, w, _) -> n ^ ": " ^ w) (triples r "refused")))
|
||||
end;
|
||||
(* One slot by index, which is the inspector's own root rather than
|
||||
[locals]' listing, and an aggregate for it: an x86 frame passes every
|
||||
aggregate by pointer, so a struct is where a recorded address could
|
||||
most easily be the caller's copy instead of this frame's. *)
|
||||
let r = request c "(:op \"inspect\" :frame 0 :slot 2)" in
|
||||
if status r <> "ok" then fail "x86 inspect: %s" (said r)
|
||||
else if Wire.string_field r "value" <> Some "(Point {.x 1.5 .y 2.5})" then
|
||||
fail "x86 inspect of slot 2 answered %S"
|
||||
(Option.value ~default:"" (Wire.string_field r "value"));
|
||||
(* And the globals, which are not in the frame at all -- they are found
|
||||
through the same descriptor's fingerprint, and a frame whose
|
||||
[refsig] disagreed with what the daemon recomputes would refuse
|
||||
every one of them while the locals above still read. That is the
|
||||
failure an empty globals table in [X86.layout_ctx] produces, and it
|
||||
is why this question is asked here and not left to the LLVM block. *)
|
||||
let r = request c "(:op \"globals\")" in
|
||||
if status r <> "ok" then fail "x86 globals: %s" (said r)
|
||||
else
|
||||
(match triples r "globals" with
|
||||
| [ ("ticks", "i64", v) ] when v <> "" -> ()
|
||||
| got ->
|
||||
fail "x86 globals: %s"
|
||||
(String.concat ", "
|
||||
(List.map (fun (n, ty, v) -> n ^ " " ^ ty ^ " = " ^ v) got)))
|
||||
end;
|
||||
ignore (request c "(:op \"close\")");
|
||||
(try Unix.close c with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] ipid) with Unix.Unix_error _ -> ())
|
||||
end;
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ isock; iout ];
|
||||
|
||||
(* And the *merged* daemon on this backend, which used to be refused by
|
||||
name and is the case the refusal was standing in for.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user