diff --git a/TODO.org b/TODO.org index a76e8535..8ac565ac 100644 --- a/TODO.org +++ b/TODO.org @@ -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 diff --git a/lib/x86.ml b/lib/x86.ml index 8202c26d..168c54cc 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -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;