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:
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