kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 33f151e8d12d1f3341e5d0a0218fc54549d53279
parent 32e99c0ce51b72eb9974aaf7abf9b7b3d2621121
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 05:46:10 -0700

harden: zero-init stack vars + obj-format secname vtable (D.3/D.4)

Add -ftrivial-auto-var-init=zero (mk/flags.mk AUTO_INIT_CFLAGS, compiler-probed
so bootstrap's kit-cc $(CC) degrades gracefully rather than erroring). Standard
zero-init hardening that also suppresses a latent, layout-sensitive
uninitialized-STACK read in the `kit run` in-process compile+link pipeline: it
flakily dropped soft-float runtime helpers (__muldf3/__divdf3/__floatsidf/...),
dying with "link: undefined reference". Pre-existing and dormant at HEAD; the
obj-format dedup below tripped it when built without the flag. Fully suppressed
with the flag (test-toy 1394/0/33 across repeated runs; 0/120 link-error probe).
The actual read still wants a real fix via MSan-on-Linux; see doc/plan/TODO.md.

D.4: drop the dead ObjFormatImpl.bin_fmt field (only ever written; the live
KIT_BIN_*->ObjFmt mapping is obj_format_lookup_bin's switch, unchanged).
D.3: move the five obj_secname_* per-format switches to ObjFormatImpl vtable
fields (secname_{init,fini,preinit}_array / tdata / tbss). NULL means the format
can't represent that section (panics, same as the old default arm). Rationale
comments move next to the per-format initializers in registry.c.

Also documents the latent bug + a follow-up to teach `kit cc` the flag.

Verified: test-toy x3, test-elf/coff/macho/link/cg-api all green.

Diffstat:
Mdoc/plan/TODO.md | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/flags.mk | 12++++++++++++
Msrc/obj/format.h | 15++++++++++++++-
Msrc/obj/obj_secnames.c | 97++++++++++++++++++-------------------------------------------------------------
Msrc/obj/registry.c | 32++++++++++++++++++++++++++++----
5 files changed, 235 insertions(+), 80 deletions(-)

diff --git a/doc/plan/TODO.md b/doc/plan/TODO.md @@ -4,3 +4,162 @@ This file is an open-backlog catalog, not a completion ledger. When an item is fixed, remove it instead of checking it off or keeping a closed entry. Add new deferred fixes below as they are discovered. + +--- + +## Known bugs / correctness gaps + +- **Latent uninitialized-stack-variable read in the `kit run` in-process compile+link + pipeline (layout-sensitive heisenbug). MITIGATED by `-ftrivial-auto-var-init=zero`; + not yet root-caused.** `kit run` on an FP program flakily dies with `link: undefined + reference to '__muldf3'` (or `__divdf3` / `__floatsidf` / `__fixdfdi` — whichever + soft-float compiler-rt helper the program needs). **Pre-existing and DORMANT at HEAD** + (0 failures in 240+ runs), but woken by memory-layout shifts: e.g. the D.3+D.4 + obj-format dedups trip it (~10–100% per build, varying run-to-run) when built WITHOUT + the hardening flag, while 4 dummy padding fields in the same struct do not — a latent + landmine, not caused by any one refactor. **`-ftrivial-auto-var-init=zero` (mk/flags.mk + `AUTO_INIT_CFLAGS`) fully suppresses it:** a clean build with D.3+D.4 + the flag shows + 0/120 genuine link-error flakes and `test-toy` 1394 pass / 0 fail across repeated + runs, so the read is from an uninitialized *stack* auto. The flag is the standard + zero-init hardening and is now on; this entry stays open because the flag MASKS the UB + rather than fixing the specific read. (Note: an earlier "8/80 with the flag" reading + was a probe bug — case `140_fp_callee_save_bottom_frame` has `.expected`=964, which + wraps to exit code 196 under an 8-bit exit-code compare; it is not a real failure.) + Characterization (all verified): + - Only manifests in the multi-source in-process pipeline. Compiling `rt/lib/fp/fp.c` + (which defines the missing helper) *alone* is 100% deterministic (50/50 clean); + the flake needs many sources compiled+linked in one process. + - **ASAN + UBSan never fire** → it is an uninitialized-memory READ (those sanitizers + don't detect uninit reads). It is a STACK auto: `-ftrivial-auto-var-init=zero` + (which only touches automatic storage) fully suppresses it. `segvec` zeroes its + slots and `obj_symbol_make` is clean, so the ObjSym/LinkSymbol tables are NOT the + source — look at per-source-compile locals in the driver pipeline. + - Real-fix path: the definitive tool is **MemorySanitizer**, unsupported on + macOS/arm64. On the arm64 Linux container (scripts/linux_bootstrap.sh), build kit + under `-fsanitize=memory`, TEMPORARILY drop `AUTO_INIT_CFLAGS`, and run a flaky FP + repro (`kit run -O0 test/toy/cases/54_scalar_intrinsics.toy` in a loop) to get the + exact file:line, then initialize that local. The hardening flag can stay regardless. + +- **Support `-ftrivial-auto-var-init=zero|pattern` in `kit cc`.** The host build now + hardens with this flag (mk/flags.mk `AUTO_INIT_CFLAGS`), but it is probed and only + applied when `$(CC)` accepts it — so it is silently dropped for bootstrap stages + where `$(CC)=kit cc`, which does not yet implement the flag. Teach the C frontend to + parse `-ftrivial-auto-var-init=zero` / `=pattern` / `=uninitialized` and emit the + corresponding implicit zero/pattern store for every automatic variable whose storage + is not fully written before first read (the same semantics as clang/gcc). This (a) + extends the same hardening to kit-built-by-kit, and (b) makes the latent uninit-read + bug above suppressed in self-hosted builds too. Until then a bootstrap-only build + (`make bootstrap-*`, where `$(CC)=kit cc`) does not get the hardening, so it could + still trip the latent flake. + +- **c_target `memory.grow` backing-store bug.** `memory_grow_large/C` fails + (`expected 42 got 139`, SIGSEGV): a `(memory 1 300)` module grows to 300 pages + then stores/loads at ~18.7 MB and segfaults — the C backend's `memory.grow` + appears not to realloc the backing store to the grown page count. Pre-existing + (confirmed failing identically at `de9bface`). Surfaces in `test-cbackend`'s + wasm-front sub-suite. Needs a c_target wasm memory-model fix. + +- **aa64/x64 inline-asm operand-staging gap (A.3).** A register-constrained asm + operand that regalloc could not keep in a hard register (e.g. an address-taken + / spilled local) is staged to a scratch reg and succeeds on rv64, but **panics** + on aa64/x64 (`"register asm operand not in a register"`). Three gaps in the + optimizer-path `*_asm_block_native`: (a) register-constrained **outputs** in a + non-reg loc have no fallback on aa64 (`src/arch/aa64/native.c:4883`) / x64 + (`:4021`), only rv64 (`src/arch/riscv/native.c:3780`); (b) **FP-constrained + inputs** (`"w"` aa64, `"x"/"v"` x64) in a non-reg loc; (c) x64 **`"q"`** inputs + (input-stage test is raw `body[0]=='r'`, so `q`→INT,allowed=0 skips staging then + panics). rv64's extra staging branch is correct (it has dedicated stage regs + `RV_TMP2/3`+`RV_FTMP0/1`). Fix aa64/x64 to match, with test cases — this changes + which programs compile vs panic, so it must be a reviewed change, NOT folded into + a dedup. Unblocks A.2 (below). + +- **`kit_dep_iter` system-path conflation (T1.12).** `kit_dep_iter_next` + (`src/api/compile.c:654-655`) reports both `from_system_path` and `bracketed` + from the single `<>`-vs-`""` spelling bit, so a `<...>` include resolved via a + plain `-I` dir falsely reports `from_system_path=1`. The accurate signal (the + resolved dir's system flag) is discarded at resolution. A correct fix needs a + cross-file change: widen `SourceInclude` (`src/core/core.h`) with a resolved-system + bit and thread it through `lang/cpp/pp/pp_directive.c` (`find_and_open_include` → + `do_include`), `pp.{c,h}`, `include/kit/source.h`, `src/api/source.c`, + `src/core/source.c`. Currently documented at the assignment site; value is + identical to before. + +## Design-needed (not a quick fix) + +- **c_target data relocations (T1.3 / D.5).** `c_emit_data_symbol` + (`src/arch/c_target/c_emit.c:3846, 3911`) hardcodes reloc width `(kind==R_ABS32)?4:8` + and renders every reloc as an absolute pointer; a sub-pointer-width address reloc + (`@pcrel` into a 4-byte `i32` slot, e.g. toy tests 96/100) overflows the slot. + Root issue is fundamental: the C backend cannot put a real `&sym` (8 bytes) into a + 4-byte slot in portable C, and a true PC-relative/section-diff offset is not a C + constant expression. Needs a design decision: **either** reject such relocs + (`compiler_panic`) and mark tests 96/100 `SKIP` on `/C`, **or** emit runtime + init-time fixups. (A naive panic-on-non-absolute was tried and reverted — it broke + 96/100, which intentionally exercise pcrel-in-data on every backend incl. C.) + +## Deferred dedups & abstraction cleanups ("use the shared seam") + +- **A.2 — optimizer-path inline-asm dedup.** The three `aa_/x64_/rv_asm_block_native` + share an out/in/bind/run/store skeleton but diverge by *policy* (staging coverage + + structurally different scratch-register models: aa64/x64 reuse 2 mem-scratch regs via + a shared `ntmp`; rv64 uses 4 dedicated stage regs). A hooks dedup would relocate, not + unify, behavior and add miscompile risk in a hot path. Do **A.3** first (make the three + staging policies identical), then a shared `native_asm_bind_optimizer_operands` becomes + safe. (The -O0 direct path is already shared via `native_asm_bind_direct_operands`.) +- **B.5 — `register_at_public` dedup.** The three arch `register_at_public` wrappers are + near-identical (differ only by the per-arch `register_iter_get` callee), but the clean + shared home needs `src/arch/arch.h` (the `ArchImpl.register_at` field signature) + + `include/kit/arch.h` (`KitArchReg`). (`backend_make`/`semantic_target_new` already + deduped into `cg/native_direct_target`.) +- **C.4 — rv32 ELF reloc map / PLT clone rv64.** `src/obj/elf/reloc_riscv32.c` clones the + rv64 reloc map (only ~3 cases differ); the rv32 PLT/IPLT emitters clone rv64 (one + LW-vs-LD instruction). Parameterize on word size. +- **C.5 — RISC-V float-ABI → `e_flags` mapping** is inlined in both the ET_REL writer + (`src/obj/elf/emit.c:698-710`) and the ET_EXEC writer (`src/obj/elf/link.c:1556`). + Extract one helper; don't encode arch direction in the format-generic ELF layer. +- **E.3 — OS-neutral env code duplicated** between `driver/env/posix.c` and `windows.c` + (stdio writer + thunks, `env→{context,jit_host,dbg_host}` trio, dir-handle structs + + read/close, `read_stdin` grow/shrink). Move to `driver/env/common.c`. **Blocked on a + Windows cross-build** to verify `windows.c` doesn't break (it can't be host-compiled). +- **E.5 — cc/build glue.** basename-stem+extension synthesis is triplicated + (`cc_default_obj_path_for_name`, `cc_dep_default_target`, `build_default_obj_name`); its + home is the driver util layer (`driver/`), not `link_inputs.c`. (The link pipeline + + flag-parse blocks were assessed and are not byte-identical — different option structs.) +- **F.2 — ELF strtab builder.** `objbb_append_str`'s dedup strtab builder is reimplemented + as `StrBuilder`/`strtab_add` ~4× in `src/obj/elf/link.c`. Use the shared builder. +- **F.3 — LEB128 + dwarf attr-skip.** LEB128 encoders are forked in `src/debug/debug_emit.c`; + the `dw_skip_die_attrs` loop is inlined ~6× in the dwarf reader. One encoder + one skip + helper. +- **F.4 — path-join triplicated** (2 copies in one `src/dist/` file). (The `ctx->diag` + varargs wrapper part of this finding is done — now `kit_ctx_diagf` in `core/diag`.) +- **G.4 — RV disassembler dispatches sub-format by `strcmp`** on the mnemonic display + string (`src/arch/riscv/disasm.c`) rather than an encoding tag. +- **I.1 — C parser shadow stack** is a 3-array struct-of-arrays + (`lang/c/parse/cg_adapter.c:53-141`) whose dup/swap/rot3/grow shuffle 3 lanes in + lockstep; collapse into one `PcgSlot{type;flags;aux;}` array. (Touches the parser↔CG + bridge — higher risk; needs care.) +- **I.2 — two parallel C-6.7.9 initializer walkers** (`lang/c/parse/parse_init.c:648-777`) + re-encode the same traversal grammar. Factor a single grammar driver + leaf vtable. +- **I.3 — `cg_adapter` callers poke the shadow-stack array directly** because no + retag-keep-flags op exists. Add the op. (Pairs with I.1.) +- **I.4 — opt passes re-hand-roll the centralized operand-walk.** (The `ranges_overlap` + trampoline part is fixed.) +- **I.5 — misc confusing constructs:** the recursive `order_dfs` stack hazard, and the + threaded-but-ignored `CSemAssignContext`. (The obfuscated `max()` and the dep-iter + field conflation are tracked elsewhere — the latter is T1.12.) + +## God-function decompositions (highest risk / lowest ROI — do when next touching) + +- `link_emit_elf` (`src/obj/elf/link.c`, ~940 lines, 14 phases incl. inline PIE + dynamic-emit that is logically the emit-half of `layout_dyn`). +- Mach-O `plan_layout` (`src/obj/macho/link.c`, ~574 lines; inline `sizeofcmds` + prediction ~1500 lines from the actual emit — a drift hazard). +- `jit_append_obj_inner` (`src/link/link_jit.c`, ~482 lines reimplementing the AOT + pipeline; had a verbatim-duplicated duplicate-global check). +- `wasm_emit_cg_into` (`lang/wasm/cg.c`, ~1960 lines fusing module setup with a + 174-case opcode switch; hoist `WasmCgControl` to file scope). +- `c_emit_data_symbol` (`src/arch/c_target/c_emit.c`, ~306 lines; doubled reloc-walk). + +## Cross-platform test failures + +(Populated by the serial cross-platform run — macOS native, Linux, FreeBSD, Windows.) diff --git a/mk/flags.mk b/mk/flags.mk @@ -33,7 +33,18 @@ endif # Darwin, empty elsewhere. Stage/bootstrap recipes can clear these when # kit itself is the compiler. +# Auto-initialize automatic (stack) variables to zero. Hardening that defines +# the otherwise-UB of reading an uninitialized local — the same default the +# Linux kernel / Android / Chrome ship; =zero is deterministic and effectively +# free at our opt levels. It also neutralizes the latent layout-sensitive +# uninit-read in the kit-run pipeline documented in doc/plan/TODO.md (that +# entry stays open until MSan pinpoints and fixes the actual read). Probed so a +# compiler without the flag — notably $(CC)=kit cc in bootstrap stages — simply +# omits it instead of erroring. +AUTO_INIT_CFLAGS = $(shell $(CC) -ftrivial-auto-var-init=zero -x c -E /dev/null >/dev/null 2>&1 && printf '%s' -ftrivial-auto-var-init=zero) + CFLAGS_COMMON = $(HOST_OPTFLAGS) $(HOST_MODE_CPPFLAGS) $(HOST_MODE_CFLAGS) \ + $(AUTO_INIT_CFLAGS) \ -std=c11 -Wpedantic -Wall -Wextra -Werror HOST_CFLAGS = $(CFLAGS_COMMON) $(HOST_SYSROOT_CFLAGS) HOST_LDFLAGS = $(HOST_SYSROOT_LDFLAGS) $(HOST_MODE_LDFLAGS) $(HOST_ENV_LDFLAGS) @@ -53,6 +64,7 @@ $(BUILD_CONFIG): FORCE printf '%s\n' 'HOST_MODE_CPPFLAGS=$(HOST_MODE_CPPFLAGS)'; \ printf '%s\n' 'HOST_MODE_CFLAGS=$(HOST_MODE_CFLAGS)'; \ printf '%s\n' 'HOST_MODE_LDFLAGS=$(HOST_MODE_LDFLAGS)'; \ + printf '%s\n' 'AUTO_INIT_CFLAGS=$(AUTO_INIT_CFLAGS)'; \ } > $@.tmp @if ! cmp -s $@.tmp $@; then mv $@.tmp $@; else rm -f $@.tmp; fi diff --git a/src/obj/format.h b/src/obj/format.h @@ -139,7 +139,6 @@ typedef struct ObjFormatEmuOps { typedef struct ObjFormatImpl { ObjFmt kind; - KitBinFmt bin_fmt; const char* name; const char* read_name; const char* read_dso_name; @@ -182,6 +181,20 @@ typedef struct ObjFormatImpl { * obj_format_weak_extern_underscore_alias. */ u8 weak_extern_underscore_alias; + /* ---- Synthetic linker-section spellings ---- + * + * Canonical names the linker assigns to the synthetic sections it builds + * (ctor/dtor arrays + TLS) at synthesis time, before any writer sees them. + * The kit-internal section model is otherwise format-neutral (ELF-spelled + * end to end). A NULL entry means the format cannot represent that section, + * and the matching obj_secname_* wrapper panics. Read only via those + * wrappers in obj_secnames.c. */ + const char* secname_init_array; /* ctor table */ + const char* secname_fini_array; /* dtor table */ + const char* secname_preinit_array; /* pre-init ctors */ + const char* secname_tdata; /* TLS initialized data */ + const char* secname_tbss; /* TLS zero-fill */ + /* ---- C-source backend (c_target) emission spellings ---- * * The portable C backend re-emits symbols as C source; a few constructs diff --git a/src/obj/obj_secnames.c b/src/obj/obj_secnames.c @@ -138,94 +138,41 @@ static Sym secname_panic_unimpl(Compiler* c, const char* which) { return 0; } +/* Resolve a synthetic-section name from the active object format's vtable. + * A NULL field means the format can't represent that section, so panic with + * the ELF-spelled `which` for the diagnostic. The per-format spellings (and + * their rationale) live on ObjFormatImpl in src/obj/registry.c. */ +static Sym obj_secname_field(Compiler* c, const char* name, const char* which) { + if (!name) return secname_panic_unimpl(c, which); + return pool_intern_cstr(c->global, name); +} + Sym obj_secname_init_array(Compiler* c) { - switch (c->target.obj) { - case KIT_OBJ_ELF: - return pool_intern_slice(c->global, SLICE_LIT(".init_array")); - case KIT_OBJ_MACHO: - return pool_intern_slice(c->global, SLICE_LIT("__DATA,__mod_init_func")); - case KIT_OBJ_COFF: - /* CRT runtime scans `.CRT$X[A-Z]` for ctor/dtor tables; XCU is - * the user-constructor bucket. See doc/OBJ.md. */ - return pool_intern_slice(c->global, SLICE_LIT(".CRT$XCU")); - default: - return secname_panic_unimpl(c, ".init_array"); - } + const ObjFormatImpl* fmt = obj_format_lookup(c->target.obj); + return obj_secname_field(c, fmt ? fmt->secname_init_array : NULL, + ".init_array"); } Sym obj_secname_fini_array(Compiler* c) { - switch (c->target.obj) { - case KIT_OBJ_ELF: - return pool_intern_slice(c->global, SLICE_LIT(".fini_array")); - case KIT_OBJ_MACHO: - return pool_intern_slice(c->global, SLICE_LIT("__DATA,__mod_term_func")); - case KIT_OBJ_COFF: - /* `.CRT$XPA`/`XPZ` are markers; XPU is the user-destructor - * bucket. See doc/OBJ.md. */ - return pool_intern_slice(c->global, SLICE_LIT(".CRT$XPU")); - default: - return secname_panic_unimpl(c, ".fini_array"); - } + const ObjFormatImpl* fmt = obj_format_lookup(c->target.obj); + return obj_secname_field(c, fmt ? fmt->secname_fini_array : NULL, + ".fini_array"); } Sym obj_secname_preinit_array(Compiler* c) { - switch (c->target.obj) { - case KIT_OBJ_ELF: - return pool_intern_slice(c->global, SLICE_LIT(".preinit_array")); - case KIT_OBJ_MACHO: - /* Mach-O has no direct `.preinit_array` analogue — dyld runs - * S_MOD_INIT_FUNC_POINTERS only. Phase 3 of the linker will - * route preinit ctors through __mod_init_func; until then any - * caller hitting this on a MACHO target is doing something the - * platform can't represent. */ - return secname_panic_unimpl(c, ".preinit_array"); - case KIT_OBJ_COFF: - /* CRT's own setup runs in `.CRT$XI*`; user pre-init lives at - * XIA just after the CRT. See doc/OBJ.md. */ - return pool_intern_slice(c->global, SLICE_LIT(".CRT$XIA")); - default: - return secname_panic_unimpl(c, ".preinit_array"); - } + const ObjFormatImpl* fmt = obj_format_lookup(c->target.obj); + return obj_secname_field(c, fmt ? fmt->secname_preinit_array : NULL, + ".preinit_array"); } Sym obj_secname_tdata(Compiler* c) { - switch (c->target.obj) { - case KIT_OBJ_ELF: - return pool_intern_slice(c->global, SLICE_LIT(".tdata")); - case KIT_OBJ_MACHO: - return pool_intern_slice(c->global, SLICE_LIT("__DATA,__thread_data")); - case KIT_OBJ_COFF: - /* MSVC `.tls$` convention; linker concatenates `.tls$*` sorted - * by suffix. See doc/OBJ.md. */ - return pool_intern_slice(c->global, SLICE_LIT(".tls$")); - case KIT_OBJ_WASM: - /* Wasm has no thread-local storage model: a module instance owns a - * single linear memory, so a thread-local is just an ordinary - * data object. Keep the `.tdata` name (laid out like `.data`) and - * lower tls_addr_of to a plain symbol address. */ - return pool_intern_slice(c->global, SLICE_LIT(".tdata")); - default: - return secname_panic_unimpl(c, ".tdata"); - } + const ObjFormatImpl* fmt = obj_format_lookup(c->target.obj); + return obj_secname_field(c, fmt ? fmt->secname_tdata : NULL, ".tdata"); } Sym obj_secname_tbss(Compiler* c) { - switch (c->target.obj) { - case KIT_OBJ_ELF: - return pool_intern_slice(c->global, SLICE_LIT(".tbss")); - case KIT_OBJ_MACHO: - return pool_intern_slice(c->global, SLICE_LIT("__DATA,__thread_bss")); - case KIT_OBJ_COFF: - /* sorted-alphabetically-last so it falls at the tail of the TLS - * image's zero-fill region. See doc/OBJ.md. */ - return pool_intern_slice(c->global, SLICE_LIT(".tls$ZZZ")); - case KIT_OBJ_WASM: - /* See obj_secname_tdata: wasm thread-locals are ordinary - * (zero-filled) data. */ - return pool_intern_slice(c->global, SLICE_LIT(".tbss")); - default: - return secname_panic_unimpl(c, ".tbss"); - } + const ObjFormatImpl* fmt = obj_format_lookup(c->target.obj); + return obj_secname_field(c, fmt ? fmt->secname_tbss : NULL, ".tbss"); } int obj_format_extern_via_got(const Compiler* c) { diff --git a/src/obj/registry.c b/src/obj/registry.c @@ -310,7 +310,6 @@ static const ObjCoffArchOps* obj_coff_machine(u16 machine) { #if KIT_OBJ_WASM_ENABLED static const ObjFormatImpl obj_format_impl_wasm = { .kind = KIT_OBJ_WASM, - .bin_fmt = KIT_BIN_WASM, .name = "wasm", /* read_wasm parses a core module into sections + function symbols for * inspection (objdump -f/-h/-t/-s/-d). It is not a linkable-object reader: @@ -330,13 +329,17 @@ static const ObjFormatImpl obj_format_impl_wasm = { .tls_symbol_features = 0, .alias_via_thunk = 0, .weak_undef_attr = "weak", + /* Wasm has no TLS model: a module instance owns one linear memory, so a + * thread-local is an ordinary data object. Keep `.tdata`/`.tbss` (laid + * out like `.data`); tls_addr_of lowers to a plain symbol address. */ + .secname_tdata = ".tdata", + .secname_tbss = ".tbss", }; #endif #if KIT_OBJ_ELF_ENABLED static const ObjFormatImpl obj_format_impl_elf = { .kind = KIT_OBJ_ELF, - .bin_fmt = KIT_BIN_ELF, .name = "elf", .read_name = "read_elf", .read_dso_name = "read_elf_dso", @@ -355,6 +358,11 @@ static const ObjFormatImpl obj_format_impl_elf = { .tls_symbol_features = 1, .alias_via_thunk = 0, .weak_undef_attr = "weak", + .secname_init_array = ".init_array", + .secname_fini_array = ".fini_array", + .secname_preinit_array = ".preinit_array", + .secname_tdata = ".tdata", + .secname_tbss = ".tbss", .elf_arch = obj_elf_arch, .elf_machine = obj_elf_machine, }; @@ -363,7 +371,6 @@ static const ObjFormatImpl obj_format_impl_elf = { #if KIT_OBJ_MACHO_ENABLED static const ObjFormatImpl obj_format_impl_macho = { .kind = KIT_OBJ_MACHO, - .bin_fmt = KIT_BIN_MACHO, .name = "macho", .read_name = "read_macho", .read_dso_name = "read_macho_dso", @@ -380,6 +387,14 @@ static const ObjFormatImpl obj_format_impl_macho = { .tls_symbol_features = 1, .alias_via_thunk = 1, .weak_undef_attr = "weak_import", + .secname_init_array = "__DATA,__mod_init_func", + .secname_fini_array = "__DATA,__mod_term_func", + /* Mach-O has no .preinit_array analogue — dyld runs S_MOD_INIT_FUNC + * pointers only — so preinit ctors are not representable (NULL panics + * until the linker routes them through __mod_init_func). */ + .secname_preinit_array = NULL, + .secname_tdata = "__DATA,__thread_data", + .secname_tbss = "__DATA,__thread_bss", .macho_arch = obj_macho_arch, .macho_cputype = obj_macho_cputype, }; @@ -388,7 +403,6 @@ static const ObjFormatImpl obj_format_impl_macho = { #if KIT_OBJ_COFF_ENABLED static const ObjFormatImpl obj_format_impl_coff = { .kind = KIT_OBJ_COFF, - .bin_fmt = KIT_BIN_COFF, .name = "coff", .read_name = "read_coff", .read_dso_name = "read_coff_dso", @@ -405,6 +419,16 @@ static const ObjFormatImpl obj_format_impl_coff = { .alias_via_thunk = 0, .weak_undef_attr = "weak", .weak_extern_underscore_alias = 1, + /* CRT scans `.CRT$X[A-Z]` for ctor/dtor tables: XCU is the user-ctor + * bucket, XPU the user-dtor bucket, XIA user pre-init just after the + * CRT's own `.CRT$XI*` setup. TLS uses the MSVC `.tls$` convention + * (linker concatenates `.tls$*` sorted by suffix; ZZZ sorts last so + * `.tbss` lands at the tail zero-fill). See doc/OBJ.md. */ + .secname_init_array = ".CRT$XCU", + .secname_fini_array = ".CRT$XPU", + .secname_preinit_array = ".CRT$XIA", + .secname_tdata = ".tls$", + .secname_tbss = ".tls$ZZZ", /* synth_inputs: the COFF __CTOR_LIST__/__DTOR_LIST__ + __chkstk * synthesizer (link_synth_coff_ctor_dtor_list) genuinely needs Linker * internals (LinkInput append, link_arch_desc_for); it cannot be