The x86 backend describes every frame it emits for an unwinder, in a release build as in a debug one

This commit is contained in:
Joseph Ferano 2026-09-25 10:05:59 +07:00
parent bf827dc55b
commit 96be1193c0
2 changed files with 37 additions and 34 deletions

View File

@ -1122,10 +1122,12 @@ so a =declare-c= wrapper leaning on the courtesy is already backend-dependent as
well as slice-dependent. The contract is pointer and length, and nothing promised
otherwise.
** NEXT Frame descriptions are gated on --debug
Decided 2026-09-25: emit the x86 backend's =.cfi= directives in every build. No runtime cost; the lane measures the =.eh_frame= size it adds.
They are correct in every build and free at runtime, and a release build is where
a crash would most want them. One =if= in three places.
** DONE Frame descriptions are gated on --debug
CLOSED: [2026-09-25]
The x86 backend emits its =.cfi= directives in every build, redefinition modules
included, so an unwinder never has to guess at a Flan frame. Cost: about 40 bytes
of =.eh_frame= and =.eh_frame_hdr= per function, 2 KB on =json.flan='s 210 KB
binary.
** TODO A !DILexicalBlock per Let
Inside nested =let=s that bind the same name, a debugger still answers with the

View File

@ -3454,12 +3454,8 @@ and cast f (a : Tast.expr) (target : Types.t) dst =
the return address is column 16 and the CIE already says it is at
[cfa-8].
Emitted only in a [--debug] build, so that a release build's assembly stays
byte-for-byte what it was. That is a conservative call rather than a
principled one: this description is correct in every build, and a release
build is where an unwind through a crash would most want it. What stops it
from being unconditional today is only that nothing measures the [.eh_frame]
it would add. *)
Emitted in every build, [--debug] or not. It costs nothing at run time,
and a release build is where an unwind through a crash most wants it. *)
let cfi_after_push b = text b "\t.cfi_def_cfa_offset 16\n\t.cfi_offset 6, -16\n"
let cfi_after_mov b = text b "\t.cfi_def_cfa_register 6\n"
let cfi_after_leave b = text b "\t.cfi_def_cfa 7, 8\n"
@ -3711,7 +3707,6 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
column even when it is on the same line, so it gets a row of its own, and
two rows are what let a debugger put a breakpoint after the prologue
rather than on it. *)
let cfi = match dw with None -> false | Some _ -> true in
let sub =
match dw with
| None -> None
@ -4069,9 +4064,9 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
% 16 == 0 at every call site below is a property of that one rounded sub rather \
than an invariant each case has to keep.";
push_r pb rbp;
if cfi then cfi_after_push pb;
cfi_after_push pb;
mov_rr pb ~dst:rbp ~src:rsp;
if cfi then cfi_after_mov pb;
cfi_after_mov pb;
let n = frame_bytes f in
if n > 0 then sub_imm pb ~dst:rsp n;
(match sret_at with
@ -4184,7 +4179,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
load_scalar f ~reg:(if is_float fn.Tast.ret then xmm0 else rax)
~off:f.retval fn.Tast.ret;
leave f.b;
if cfi then cfi_after_leave f.b;
cfi_after_leave f.b;
ret f.b;
flush pb;
flush f.b;
@ -4206,7 +4201,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
if hidden then Buffer.add_string out (Printf.sprintf "\t.hidden\t%s\n" sym);
Buffer.add_string out (Printf.sprintf "\t.type\t%s, @function\n" sym);
Buffer.add_string out (sym ^ ":\n");
if cfi then Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out (Buffer.contents pb.out);
Buffer.add_string out (Buffer.contents f.b.out);
(* One past the last byte, which is what [DW_AT_high_pc] and the line
@ -4217,7 +4212,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
| Some s -> Buffer.add_string out (s.send ^ ":\n")
| None -> ());
(match dw with Some d -> d.dcur <- None | None -> ());
if cfi then Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out (Printf.sprintf "\t.size\t%s, . - %s\n\n" sym sym);
Buffer.contents out, Buffer.contents f.rodata
@ -4254,7 +4249,7 @@ let init_sym = asm_sym (Mangle.sym ".init-globals")
the loader put them in, the program's own end of the transfer channel is a
null cell on this frame, and the exit goes through [flan_exit] because
stdout is a FILE* and something has to flush it. *)
let emit_main ?(cfi = false) ?(ann = false) ?(startup = false) ?(gc = false)
let emit_main ?(ann = false) ?(startup = false) ?(gc = false)
?(dyn_globals = []) (md : Emit.m) (fn : Tast.fn) =
let b = create () in
bnote ann b
@ -4266,9 +4261,9 @@ let emit_main ?(cfi = false) ?(ann = false) ?(startup = false) ?(gc = false)
and something has to flush it. The ud2 at the end is unreachable — flan_exit does \
not return.";
push_r b rbp;
if cfi then cfi_after_push b;
cfi_after_push b;
mov_rr b ~dst:rbp ~src:rsp;
if cfi then cfi_after_mov b;
cfi_after_mov b;
sub_imm b ~dst:rsp 48;
(* [al] is zero at every call this backend makes, variadic or not — see
[call_native]. Setting it here too costs two bytes and keeps the rule
@ -4380,9 +4375,9 @@ let emit_main ?(cfi = false) ?(ann = false) ?(startup = false) ?(gc = false)
it leaves through [flan_exit] and the [ud2] after that is unreachable.
The rbp rule therefore holds to the last byte, which is what a backtrace
out of anything [main] called needs. *)
if cfi then Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out (Buffer.contents b.out);
if cfi then Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out "\t.size\tmain, . - main\n\n";
Buffer.contents out
@ -4409,7 +4404,7 @@ let emit_globals_data (md : Emit.m) (globals : Tast.global list) =
Buffer.contents out
let emit_globals_init ?(cfi = false) ?(ann = false) ?body ~sym (md : Emit.m) ~externs ~fns
let emit_globals_init ?(ann = false) ?body ~sym (md : Emit.m) ~externs ~fns
(globals : Tast.global list) =
let b = create () in
let f =
@ -4454,9 +4449,9 @@ let emit_globals_init ?(cfi = false) ?(ann = false) ?body ~sym (md : Emit.m) ~ex
end;
let pb = create () in
push_r pb rbp;
if cfi then cfi_after_push pb;
cfi_after_push pb;
mov_rr pb ~dst:rbp ~src:rsp;
if cfi then cfi_after_mov pb;
cfi_after_mov pb;
let n = frame_bytes f in
if n > 0 then sub_imm pb ~dst:rsp n;
(* No caller hands this one a channel, so it gets a null cell of its own and
@ -4467,16 +4462,16 @@ let emit_globals_init ?(cfi = false) ?(ann = false) ?body ~sym (md : Emit.m) ~ex
store_int pb ~src:rax ~mm:(Frame f.xfer_off) ~size:8;
lbl f.b f.retlbl;
leave f.b;
if cfi then cfi_after_leave f.b;
cfi_after_leave f.b;
ret f.b;
flush pb; flush f.b;
let out = Buffer.create 512 in
Buffer.add_string out
(Printf.sprintf "\t.type\t%s, @function\n%s:\n" sym sym);
if cfi then Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out "\t.cfi_startproc\n";
Buffer.add_string out (Buffer.contents pb.out);
Buffer.add_string out (Buffer.contents f.b.out);
if cfi then Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out "\t.cfi_endproc\n";
Buffer.add_string out (Printf.sprintf "\t.size\t%s, . - %s\n\n" sym sym);
Buffer.contents out, Buffer.contents f.rodata
@ -4832,7 +4827,7 @@ let program ~checks ?(dev = false) ?(debug = false) ?(annotate = false)
in
let computed, init_flags, init_body = Emit.startup_plan md p.Tast.globals in
let ginit, gr =
emit_globals_init ~cfi:debug ~ann:annotate ~sym:data_sym md ~externs ~fns
emit_globals_init ~ann:annotate ~sym:data_sym md ~externs ~fns
constants
in
Buffer.add_string text ginit;
@ -4840,7 +4835,7 @@ let program ~checks ?(dev = false) ?(debug = false) ?(annotate = false)
let startup = computed <> [] in
if startup then begin
let t, r =
emit_globals_init ~cfi:debug ~ann:annotate ~body:init_body ~sym:init_sym
emit_globals_init ~ann:annotate ~body:init_body ~sym:init_sym
md ~externs ~fns computed
in
Buffer.add_string text t;
@ -4849,7 +4844,7 @@ let program ~checks ?(dev = false) ?(debug = false) ?(annotate = false)
(match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns with
| Some fn ->
Buffer.add_string text
(emit_main ~cfi:debug ~ann:annotate ~startup ~gc:(Emit.uses_dyn p)
(emit_main ~ann:annotate ~startup ~gc:(Emit.uses_dyn p)
~dyn_globals:
(List.filter_map
(fun (g : Tast.global) ->
@ -5217,7 +5212,9 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
end;
let pb = create () in
push_r pb rbp;
cfi_after_push pb;
mov_rr pb ~dst:rbp ~src:rsp;
cfi_after_mov pb;
let n = frame_bytes f in
if n > 0 then sub_imm pb ~dst:rsp n;
xor_rr pb ~dst:rax ~src:rax;
@ -5226,17 +5223,18 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
store_int pb ~src:rax ~mm:(Frame f.xfer_off) ~size:8;
lbl f.b f.retlbl;
leave f.b;
cfi_after_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";
flan_reload_install:\n\t.cfi_startproc\n";
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";
"\t.cfi_endproc\n\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
@ -5252,7 +5250,9 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
| Some fn ->
let cb = create () in
push_r cb rbp;
cfi_after_push cb;
mov_rr cb ~dst:rbp ~src:rsp;
cfi_after_mov cb;
sub_imm cb ~dst:rsp 16;
xor_rr cb ~dst:rax ~src:rax;
store_int cb ~src:rax ~mm:(Frame (-8)) ~size:8;
@ -5260,15 +5260,16 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
xor_rr cb ~dst:rax ~src:rax;
call_sym cb (fsym fn);
leave cb;
cfi_after_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";
flan_reload_call:\n\t.cfi_startproc\n";
Buffer.add_buffer text cb.out;
Buffer.add_string text
"\t.size\tflan_reload_call, . - flan_reload_call\n\n");
"\t.cfi_endproc\n\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;