commit 745214299916153bb7dcb7c0f446c55d330d7efc
parent 80a4f4e82fddce173659bf6d9aec676cb9615f57
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 14:47:07 -0700
windows: resolve COFF weak-external aliases (x64 setjmp cross-link)
Fix the x86_64-windows setjmp cross-link, which had two layers.
1. read_coff dropped COFF WEAK_EXTERNAL alias targets. mingw x86_64's
`_setjmp` is a WEAK_EXTERNAL *alias* (aux Characteristics =
IMAGE_WEAK_EXTERN_SEARCH_ALIAS) to `__intrinsic_setjmp` (a short-import
from api-ms-win-crt-private-l1-1-0.dll). The reader modelled it as a bare
SB_WEAK undef and discarded the aux TagIndex, leaving resolution to a
single-underscore heuristic that can't derive `__intrinsic_setjmp` from
`_setjmp`, so the link failed with `undefined reference to '_setjmp'`.
read_coff now records the alias target (obj_set_weak_alias, guarded on
SEARCH_ALIAS), and link_resolve_undefs builds a cross-input name->target
map so any reference to the aliased name resolves to the target regardless
of which input declared the alias. New test/link/coff_weak_alias_test.c
pins the read-side decode (positive SEARCH_ALIAS + negative SEARCH_LIBRARY).
2. rt/include/setjmp.h asm-renamed setjmp->__mingw_setjmp for all _WIN32, but
kit.exe's own freestanding TUs use this header and __mingw_setjmp exists
only in mingw's aarch64 runtime. Make it arch-aware: aarch64/arm keep
__mingw_setjmp/__mingw_longjmp; x86_64 uses mingw's non-SEH idiom
setjmp(env) -> _setjmp((env), NULL) (resolved via layer 1) + ucrt longjmp.
scripts/windows_cross.sh x64 now links a complete x64 kit.exe (PE32+) for the
first time. VM-verified on Prism: x64 kit.exe loads/dispatches, hash works, a
plain x64 printf program runs, and the aarch64 build of a setjmp/longjmp
program runs end-to-end. Two follow-ups recorded in doc/plan/windows.md: x64
setjmp programs fail to load on the VM (0xC0000139 on the __intrinsic_setjmp
private-apiset import), and the x64 self-host kit.exe has broad codegen crashes
in its object-reader/assembler paths.
Diffstat:
9 files changed, 460 insertions(+), 71 deletions(-)
diff --git a/doc/plan/windows.md b/doc/plan/windows.md
@@ -109,14 +109,52 @@ Concrete defects surfaced during bring-up, each blocking a roadmap item below.
and returns its exit code. The earlier *workaround* (host-cross-compile the
`.exe` on macOS to dodge the VM-side rt build) is no longer needed.
-- **x64 cross-link fails on `_setjmp`** (blocks all of §2, x86_64-windows
- parity). `scripts/windows_cross.sh x64` cannot link an x64 `kit.exe`:
- `fatal: link: undefined reference to '_setjmp'`. mingw's x86-64 `<setjmp.h>`
- routes `setjmp` to the intrinsic `_setjmp` (which takes an implicit
- frame/SSP-context arg), not the bare `__mingw_setjmp` that `rt/include/setjmp.h`
- asm-renames `setjmp` to on the aarch64 path. Needs an x64-specific setjmp shim
- (or an `_setjmp`→`__mingw_setjmp` mapping that supplies the implicit arg)
- before an x64 `kit.exe` can link. See §2.
+- **x64 cross-link setjmp gap — FIXED.** `scripts/windows_cross.sh x64` now
+ links a complete x64 `kit.exe` (PE32+). The blocker had two layers:
+ 1. **COFF weak-external aliases were dropped at read time.** mingw x86_64's
+ `_setjmp` is a COFF `WEAK_EXTERNAL` *alias* (aux `Characteristics =
+ IMAGE_WEAK_EXTERN_SEARCH_ALIAS`) to `__intrinsic_setjmp` (itself a
+ short-import from `api-ms-win-crt-private-l1-1-0.dll`). `read_coff` modelled
+ the weak external as a bare `SB_WEAK` undef and *discarded the aux
+ TagIndex*, leaving link resolution to a single-underscore naming heuristic
+ (`_foo`↔`foo`) that can't derive `__intrinsic_setjmp` from `_setjmp`. Fix:
+ `read_coff` now records the alias target (`obj_set_weak_alias`, guarded on
+ `SEARCH_ALIAS`), and `link_resolve_undefs` builds a cross-input
+ name→target map so a reference to the aliased name resolves to the target
+ regardless of which input declared the alias. Guarded by
+ `test/link/coff_weak_alias_test.c`. This alone fixes hosted user-program
+ compiles (`kit cc` of a `setjmp` program against the mingw sysroot).
+ 2. **`rt/include/setjmp.h` asm-renamed `setjmp`→`__mingw_setjmp` for all
+ `_WIN32`.** `kit.exe`'s own TUs are built freestanding (`-nostdinc
+ -Irt/include`), so they use kit's `<setjmp.h>`, not the sysroot's — and
+ `__mingw_setjmp` exists only in mingw's *aarch64* runtime. The header is now
+ arch-aware: aarch64/arm keep `__mingw_setjmp`/`__mingw_longjmp`; x86_64 uses
+ mingw's own non-SEH idiom `setjmp(env) → _setjmp((env), NULL)` (resolved via
+ layer 1) plus the real ucrt `longjmp` export. The resulting x64 `kit.exe`
+ imports `__intrinsic_setjmp` + `longjmp`.
+
+ The **cross-link** is fully fixed and VM-confirmed: the x64 `kit.exe` loads and
+ its multitool dispatch runs under Prism, and a plain x64 hosted program
+ (`hello.c`, printf, no setjmp) prints + returns its exit code. But the
+ **runtime setjmp binding is not yet correct on x64** — see the next bullet.
+
+- **x64 setjmp program fails to load on the VM: `0xC0000139`
+ STATUS_ENTRYPOINT_NOT_FOUND — NEW, OPEN.** A kit-compiled x86_64-windows
+ `setjmp`/`longjmp` program links (via the alias fix above) but **fails at load
+ time** on the Win11 ARM64 VM's x64 emulator (Prism) with `0xC0000139`. The
+ import that can't be located is `__intrinsic_setjmp` from
+ `api-ms-win-crt-private-l1-1-0.dll` (a CRT-*private* api-set). A plain printf
+ program with no setjmp loads and runs fine on the same VM, and the **aarch64**
+ build of the identical setjmp program runs end-to-end (`go` → `longjmp` →
+ `back r=42 step=1`, exit 7) — so the gap is specifically the x64 setjmp import,
+ not general loading. Resolving `_setjmp` to its weak-external alias target
+ `__intrinsic_setjmp` is *linker*-correct but binds to a private-api-set export
+ that the loader won't resolve here (whether that's a ucrtbase-export gap, an
+ api-set-forwarding gap, or Prism-specific is unconfirmed). Options to evaluate:
+ bind x64 setjmp to a *loadable* public symbol instead (e.g. import `_setjmp`
+ itself, or `setjmp`), provide setjmp from `libkit_rt` (`rt/lib/coro/x86_64_win.c`
+ already defines a weak `setjmp`) and link rt for hosted programs, or pull a
+ static thunk. Until then, x64 setjmp is link-correct but not run-correct.
## 1. JIT runtime gaps: printf-family and thread-locals — DONE
@@ -153,25 +191,45 @@ it) and its idiom relocs are dropped (link_jit.c). `141_threadlocal_mutate`
(43) and `142_threadlocal_multi` (134) now pass under JIT; a C `_Thread_local`
program mutates/reads correctly too. Regression guard:
`test/link/jit_tls_relax_test.c` pins the byte rewrite for both arches (the only
-automated coverage for the x86-64 idiom, since x64-windows can't run end-to-end
-until §2's `_setjmp` gap is closed).
+automated coverage for the x86-64 idiom, since x64-windows isn't yet verified
+end-to-end on the VM — see §2).
## 2. x86_64-windows parity
aarch64-windows is the reference. x64 codegen now shares the large-frame
-stack-probe gate (§Baseline) via `abi_stack_probe_interval`, but the self-host
-path isn't built/verified end to end:
+stack-probe gate (§Baseline) via `abi_stack_probe_interval`, and the cross-link
+now succeeds end to end. First VM run of the x64 binary surfaced two distinct
+problem areas (kit-*compiled* x64 programs vs. the x64 *self-host* `kit.exe`):
+
- Build `rt-x86_64-pc-windows` (the variant exists in `mk/rt.mk` /
- `driver/lib/runtime.c`; not built by default here).
-- `scripts/windows_cross.sh x64` should produce an x64 `kit.exe`; verify it runs
- via the VM's x64 emulator and re-run the Toy AOT corpus. **Blocked** by the
- `_setjmp` cross-link failure (see *Open bugs / blockers* above) — an x64
- `kit.exe` can't link until that's fixed. (Independently, the new
- `test/link/jit_tls_relax_test.c` already pins the x64 JIT-TLS relax bytes, so
- that path is guarded even before the self-host build is unblocked.)
-- Known x64-windows codegen gaps to expect (from the toy VM lanes): `36/37`
+ `driver/lib/runtime.c`; not built by default here). Builds cleanly.
+- `scripts/windows_cross.sh x64` **produces a complete x64 `kit.exe`** (PE32+) —
+ the cross-link gap is closed (see *Open bugs / blockers*).
+
+**VM verification status (Prism, Win11 25H2 ARM64):**
+- x64 `kit.exe` loads and dispatches (`kit` with no args prints the multitool
+ help). ✓
+- x64 `kit.exe` `hash` works (compute + file I/O, exit 0, correct digest). ✓
+- A kit-compiled x64 **non-setjmp** hosted program (printf `hello`) runs and
+ returns its exit code. ✓
+- A kit-compiled x64 **setjmp** program fails to load (`0xC0000139`) — see the
+ setjmp import blocker in *Open bugs / blockers*. The **aarch64** build of the
+ same program runs end-to-end. ✗ (x64) / ✓ (aarch64)
+- **The x64 self-host `kit.exe` itself has broad codegen/runtime breakage** (this
+ binary never linked before, so it had never run). Observed on the VM at -O0:
+ `nm`/`size` on a valid object and `as` on a source both **crash with
+ `0xC0000005`**; `cpp` and `xxd` return error (`-1`) on valid input; only
+ trivial paths (`--help`, `hash`) work. So `kit.exe` x64 is far from usable as a
+ compiler on the VM — running the Toy AOT corpus through it is blocked on
+ triaging these crashes (likely x64-windows codegen bugs in the object-reader /
+ assembler / preprocessor paths, distinct from the setjmp work). NOTE: one
+ crash class — `nm`/`size` faulting only after detection passes into the
+ `setjmp(panic.env)`-guarded `kit_obj_open` read — *might* implicate the same
+ x64 setjmp binding (layer 2 above); needs isolation.
+- Known x64-windows codegen gaps already on file (from the toy VM lanes): `36/37`
`*sret` tail-call crash at **-O1**, and `118_decl_extra_attrs` ADRP-range link
- is aarch64-only. Re-confirm against a native x64 `kit.exe`.
+ is aarch64-only. Re-confirm against a native x64 `kit.exe` once the above are
+ triaged.
## 3. A committed "compile-on-VM" test lane
diff --git a/mk/test.mk b/mk/test.mk
@@ -97,6 +97,7 @@ TEST_TARGETS = \
test-link-reloc-desc \
test-link-reloc-apply \
test-link-jit-tls-relax \
+ test-link-coff-weak-alias \
test-macho \
test-native-direct-target \
test-opt \
@@ -166,6 +167,7 @@ DEFAULT_TEST_TARGETS = \
test-link-reloc-desc \
test-link-reloc-apply \
test-link-jit-tls-relax \
+ test-link-coff-weak-alias \
test-dbg \
test-disasm-complete \
test-macho \
@@ -409,6 +411,14 @@ JIT_TLS_RELAX_TEST_BIN = build/test/jit_tls_relax_test
test-link-jit-tls-relax: $(JIT_TLS_RELAX_TEST_BIN)
$(JIT_TLS_RELAX_TEST_BIN)
+# COFF WEAK_EXTERNAL alias capture in read_coff: pins the aux-TagIndex decode
+# that lets the linker resolve mingw x86_64's `_setjmp` -> `__intrinsic_setjmp`
+# alias (the x64-windows cross-link blocker). Internal-surface (raw lib objects).
+COFF_WEAK_ALIAS_TEST_BIN = build/test/coff_weak_alias_test
+
+test-link-coff-weak-alias: $(COFF_WEAK_ALIAS_TEST_BIN)
+ $(COFF_WEAK_ALIAS_TEST_BIN)
+
# test-emu-unit: white-box unit tests for the emulator's INTERNAL units (rv64
# decoder, EmuAddrSpace, Linux syscall handler) that have no public API. Reaches
# internal symbols -> links $(LIB_OBJS) (mirrors test-interp), not the archive.
diff --git a/mk/test_unit.mk b/mk/test_unit.mk
@@ -54,7 +54,7 @@ UNIT_TESTS_INTERNAL := \
dwarf_test debug_roundtrip_unit debug_cfi_unit \
aa64_isa_test rv64_decode_test rv32_decode_test aa64_sweep_gen \
reloc_uleb128_unit reloc_desc_test reloc_apply_test emu_rv64_unit_test \
- interp_smoke_test jit_tls_relax_test \
+ interp_smoke_test jit_tls_relax_test coff_weak_alias_test \
rv64_interp_smoke_test abi_classify_test ir_recorder_test \
native_direct_target_test x64_dbg_test cg_ir_lower_test tiny_inline_test
dwarf_test_SRC := test/dwarf/dwarf_test.c
@@ -68,6 +68,7 @@ reloc_uleb128_unit_SRC := test/link/reloc_uleb128_unit.c
reloc_desc_test_SRC := test/link/reloc_desc_test.c
reloc_apply_test_SRC := test/link/reloc_apply_test.c
jit_tls_relax_test_SRC := test/link/jit_tls_relax_test.c
+coff_weak_alias_test_SRC := test/link/coff_weak_alias_test.c
emu_rv64_unit_test_SRC := test/emu/rv64_vm_unit_test.c
interp_smoke_test_SRC := test/interp/interp_smoke_test.c
rv64_interp_smoke_test_SRC := test/emu/rv64_interp_smoke_test.c
diff --git a/rt/include/setjmp.h b/rt/include/setjmp.h
@@ -25,17 +25,31 @@ typedef struct {
} jmp_buf[1];
/* On Windows (mingw) there is no bare `setjmp` symbol to link against: libc's
- * <setjmp.h> defines setjmp as a macro over _setjmpex / __mingw_setjmp. kit's
+ * <setjmp.h> defines setjmp as a macro over the per-arch intrinsic. kit's
* freestanding code wants POSIX-style pure register save/restore with no SEH
* frame unwinding (kit is arena-allocated; longjmp targets have no cleanup to
- * run), which is exactly mingw's non-SEH __mingw_setjmp / __mingw_longjmp pair
- * (libmingwex, self-contained, no .pdata dependency). Bind to them by asm-label
- * so freestanding libkit links against the mingw runtime; kit's 256-byte
+ * run). The mingw symbol that delivers that differs by architecture, so the
+ * binding below mirrors mingw's own non-SEH idiom per arch. kit's 256-byte
* jmp_buf covers mingw's aarch64/x86_64 _JBLEN. Non-Windows targets resolve
* bare setjmp from the host libc (POSIX) or libkit_rt's per-arch coro asm. */
#if defined(_WIN32)
+# if defined(__aarch64__) || defined(__arm__)
+/* aarch64/arm mingw ship the self-contained non-SEH __mingw_setjmp /
+ * __mingw_longjmp pair (libmingwex, no .pdata dependency). Bind by asm-label. */
int setjmp(jmp_buf env) __asm__("__mingw_setjmp");
_Noreturn void longjmp(jmp_buf env, int val) __asm__("__mingw_longjmp");
+# else
+/* x86_64 mingw exposes no __mingw_setjmp; its non-SEH idiom (the header's
+ * __USE_MINGW_SETJMP_NON_SEH path) is `_setjmp((buf), NULL)` — the ucrt
+ * intrinsic with a null frame so longjmp performs no SEH unwinding. `_setjmp`
+ * is a COFF weak-external alias to ucrt's `__intrinsic_setjmp`; the linker
+ * resolves that alias (see src/link/link_resolve.c). longjmp is a real ucrt
+ * export. The macro supplies the implicit frame argument; the declaration keeps
+ * the standard 1-argument setjmp prototype for callers that take its address. */
+int _setjmp(jmp_buf env, void* frame);
+_Noreturn void longjmp(jmp_buf env, int val);
+# define setjmp(env) _setjmp((env), (void*)0)
+# endif
#else
int setjmp(jmp_buf env);
_Noreturn void longjmp(jmp_buf env, int val);
diff --git a/src/link/link_resolve.c b/src/link/link_resolve.c
@@ -353,8 +353,66 @@ static LinkInputId find_dso_export(Linker* l, Sym name) {
return LINK_INPUT_NONE;
}
+/* Resolve undefined symbol `s` to the symbol named `alias` (a defined image
+ * global or a DSO export), copying the target's binding into `s`. Returns 1 on
+ * success. Shared by the recorded-alias path and the underscore heuristic. */
+static int resolve_to_alias(Linker* l, LinkImage* img, LinkSymbol* s,
+ Sym alias) {
+ if (alias == 0) return 0;
+ LinkSymId hit = symhash_get(&img->globals, alias);
+ if (hit != LINK_SYM_NONE) {
+ LinkSymbol* def = LinkSyms_at(&img->syms, hit - 1);
+ if (def->defined || def->imported) {
+ s->name = def->name;
+ s->section_id = def->section_id;
+ s->value = def->value;
+ s->vaddr = def->vaddr;
+ s->kind = def->kind;
+ s->defined = def->defined;
+ s->imported = def->imported;
+ s->dso_input_id = def->dso_input_id;
+ if (!s->defined && !s->imported) {
+ s->kind = SK_ABS;
+ s->vaddr = 0;
+ s->defined = 1;
+ }
+ return 1;
+ }
+ }
+ LinkInputId dso = find_dso_export(l, alias);
+ if (dso != LINK_INPUT_NONE) {
+ s->name = alias;
+ s->imported = 1;
+ s->dso_input_id = dso;
+ return 1;
+ }
+ return 0;
+}
+
void link_resolve_undefs(Linker* l, LinkImage* img) {
u32 i;
+
+ /* Cross-input COFF WEAK_EXTERNAL alias map: alias-declarator name -> target
+ * name (SymHash's value slot holds an interned Sym, never a real LinkSymId
+ * here; 0 = absent). Populated from every input's recorded aliases so a
+ * reference to the aliased name resolves to the target regardless of which
+ * input the reference vs. the declarator came from. Empty for non-COFF. */
+ SymHash alias_map;
+ symhash_init(&alias_map, l->heap);
+ for (u32 ii = 0; ii < LinkInputs_count(&l->inputs); ++ii) {
+ LinkInput* in = LinkInputs_at(&l->inputs, ii);
+ if (!in->obj || in->kind == LINK_INPUT_DSO_BYTES) continue;
+ u32 na = obj_weak_alias_count(in->obj);
+ for (u32 ai = 0; ai < na; ++ai) {
+ ObjSymId asym = OBJ_SYM_NONE;
+ Sym target = 0;
+ if (!obj_weak_alias_at(in->obj, ai, &asym, &target) || target == 0)
+ continue;
+ const ObjSym* os = obj_symbol_get(in->obj, asym);
+ if (os && os->name != 0) symhash_set(&alias_map, os->name, target);
+ }
+ }
+
for (i = 0; i < LinkSyms_count(&img->syms); ++i) {
LinkSymbol* s = LinkSyms_at(&img->syms, i);
if (s->defined) continue;
@@ -391,20 +449,38 @@ void link_resolve_undefs(Linker* l, LinkImage* img) {
continue;
}
}
- /* COFF WEAK_EXTERNAL alias fallback: kit drops the aux TagIndex
- * at read time (see coff_read.c step "WEAK_EXTERNAL primary"), so
- * the alias relationship is recovered here via the mingw single-
- * underscore naming convention. e.g. `__set_app_type` aliases to
- * `_set_app_type`; `__imp___set_app_type` aliases to
- * `__imp__set_app_type`. Try the de-underscored variant first,
- * then the re-underscored one. Look in both image globals (for
- * defined-in-input aliases) and DSO exports (for DLL imports).
- *
- * Applied to both WEAK (alias-declarator members) and GLOBAL
- * undefs (user references like crt2.o's call to `__set_app_type`)
- * because the alias relationship is purely a naming convention in
- * the mingw CRT — losing the aux TagIndex means we can't tell
- * which side is the alias declarator. */
+ /* COFF WEAK_EXTERNAL alias: resolve to the recorded fall-back symbol (the
+ * aux TagIndex target captured by read_coff, collected into `alias_map`
+ * keyed by the alias-declarator's name). This is the precise relationship —
+ * e.g. mingw x86_64's `_setjmp` aliasing `__intrinsic_setjmp`, which the
+ * single-underscore heuristic below cannot derive. It applies to ANY undef
+ * of the aliased name, not just the declarator symbol itself: the strong
+ * reference (sj.o's `_setjmp`) and the weak declarator are distinct undefs,
+ * and the reference is what needs redirecting. The declarator member is
+ * pulled by member_satisfies (weak undef under PE/COMDAT semantics), which
+ * brings in the target's own undef and pulls its short-import DSO, so the
+ * target is resolvable by now. Follow a short chain in case the target is
+ * itself an alias. */
+ if (s->name != 0) {
+ int resolved = 0;
+ Sym cur = s->name;
+ for (u32 hop = 0; hop < 8u; ++hop) {
+ Sym target = symhash_get(&alias_map, cur);
+ if (target == 0) break;
+ if (resolve_to_alias(l, img, s, target)) {
+ resolved = 1;
+ break;
+ }
+ cur = target;
+ }
+ if (resolved) continue;
+ }
+ /* COFF WEAK_EXTERNAL alias fallback for references that carry no recorded
+ * aux TagIndex (GLOBAL undefs like crt2.o's call to `__set_app_type`, or
+ * inputs read before alias capture): recover the relationship via the mingw
+ * single-underscore naming convention. e.g. `__set_app_type` aliases to
+ * `_set_app_type`; `__imp___set_app_type` aliases to `__imp__set_app_type`.
+ * Try the de-underscored variant first, then the re-underscored one. */
if (obj_format_weak_extern_underscore_alias(l->c) && s->name != 0) {
Slice nm_s = pool_slice(l->c->global, s->name);
const char* nm = nm_s.s;
@@ -424,37 +500,7 @@ void link_resolve_undefs(Linker* l, LinkImage* img) {
}
int resolved = 0;
for (u32 ci = 0; !resolved && ci < ncand; ++ci) {
- Sym alias = candidates[ci];
- if (alias == 0) continue;
- LinkSymId hit = symhash_get(&img->globals, alias);
- if (hit != LINK_SYM_NONE) {
- LinkSymbol* def = LinkSyms_at(&img->syms, hit - 1);
- if (def->defined || def->imported) {
- s->name = def->name;
- s->section_id = def->section_id;
- s->value = def->value;
- s->vaddr = def->vaddr;
- s->kind = def->kind;
- s->defined = def->defined;
- s->imported = def->imported;
- s->dso_input_id = def->dso_input_id;
- if (!s->defined && !s->imported) {
- s->kind = SK_ABS;
- s->vaddr = 0;
- s->defined = 1;
- }
- resolved = 1;
- break;
- }
- }
- LinkInputId dso = find_dso_export(l, alias);
- if (dso != LINK_INPUT_NONE) {
- s->name = alias;
- s->imported = 1;
- s->dso_input_id = dso;
- resolved = 1;
- break;
- }
+ if (resolve_to_alias(l, img, s, candidates[ci])) resolved = 1;
}
if (resolved) continue;
}
@@ -502,6 +548,7 @@ void link_resolve_undefs(Linker* l, LinkImage* img) {
(int)namelen, nm);
}
}
+ symhash_fini(&alias_map);
}
/* ---- pass 1b: --gc-sections liveness ---- */
diff --git a/src/obj/coff/read.c b/src/obj/coff/read.c
@@ -625,6 +625,32 @@ ObjBuilder* read_coff(Compiler* c, const char* name, const u8* data,
sym_size, cmnalign);
obj_sym_mark_referenced(ob, id);
sym_to_obj[i] = id;
+
+ /* Genuine WEAK_EXTERNAL alias declaration (IMAGE_WEAK_EXTERN_SEARCH_ALIAS):
+ * record the fall-back symbol (aux TagIndex) by name so the linker can
+ * resolve this weak symbol to its target directly. mingw x86_64 spells
+ * `_setjmp` this way, aliasing `__intrinsic_setjmp` — a redirection the
+ * link-time single-underscore heuristic can't derive. Other weak-external
+ * search policies (kit's own SB_WEAK emit uses SEARCH_LIBRARY with a
+ * self/zero TagIndex, i.e. "weak, no fallback") are left to that heuristic
+ * and the plain SB_WEAK-undef path. */
+ if (sclass == IMAGE_SYM_CLASS_WEAK_EXTERNAL && naux >= 1 && sn != 0) {
+ const u8* aux = p + COFF_SYMBOL_SIZE;
+ u32 tag_index = coff_rd_u32(aux + 0);
+ u32 characteristics = coff_rd_u32(aux + 4);
+ if (characteristics == IMAGE_WEAK_EXTERN_SEARCH_ALIAS &&
+ tag_index < nsymbols && tag_index != i) {
+ const u8* tp = sym_base + (u64)tag_index * COFF_SYMBOL_SIZE;
+ const char* tnm;
+ u32 tnlen;
+ resolve_sym_name(tp, strtab, strtab_size, &tnm, &tnlen);
+ if (tnlen != 0 && (tnlen != nlen || memcmp(tnm, nm, nlen) != 0)) {
+ Sym target =
+ pool_intern_slice(c->global, (Slice){.s = tnm, .len = tnlen});
+ obj_set_weak_alias(ob, id, target);
+ }
+ }
+ }
i += 1u + naux;
}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -31,6 +31,16 @@ SEGVEC_DEFINE(Relocs, Reloc, 6); /* 64 entries per segment */
SEGVEC_DEFINE(Groups, ObjGroup, 3); /* 8 entries per segment */
SEGVEC_DEFINE(Atoms, ObjAtom, 5); /* 32 entries per segment */
+/* COFF WEAK_EXTERNAL alias declaration: symbol `sym` is an alias for the
+ * symbol named `target`. Rare (only import-archive members and the like
+ * carry these), so a side-vector keyed by ObjSymId keeps ObjSym lean
+ * instead of growing every symbol. See obj_set_weak_alias. */
+typedef struct ObjWeakAlias {
+ ObjSymId sym;
+ Sym target;
+} ObjWeakAlias;
+SEGVEC_DEFINE(WeakAliases, ObjWeakAlias, 3); /* 8 entries per segment */
+
#define OBJ_EXT_SLOT_COUNT 6 /* OBJ_EXT_NONE..OBJ_EXT_WASM_IMPORTS */
typedef struct ObjExtSlot {
@@ -67,6 +77,10 @@ struct KitObjBuilder {
* hint/name-table entry uses this. See obj_set_coff_import_name. */
Sym coff_import_name;
u8 coff_import_name_set;
+ /* COFF WEAK_EXTERNAL alias declarations read from the input (symbol ->
+ * fallback/default symbol name). Empty on builders that carry none. See
+ * obj_set_weak_alias / obj_get_weak_alias. */
+ WeakAliases weak_aliases;
/* Cached undef extern `__tlv_bootstrap` (Mach-O on-disk name) used by
* obj_define_tls when emitting `_Thread_local` storage on Mach-O.
* Lazily materialized on the first TLV emission; OBJ_SYM_NONE otherwise. */
@@ -100,6 +114,7 @@ ObjBuilder* obj_new(Compiler* c) {
Groups_init(&ob->groups, h);
Atoms_init(&ob->atoms, h);
SymNameIndex_init(&ob->sym_by_name, h);
+ WeakAliases_init(&ob->weak_aliases, h);
/* Reserve index 0 in each id space as the "none" sentinel. SegVec
* pushes are zeroed, so the sentinel slots have all-zero fields. */
@@ -151,6 +166,7 @@ void obj_free(ObjBuilder* ob) {
Groups_fini(&ob->groups);
Atoms_fini(&ob->atoms);
SymNameIndex_fini(&ob->sym_by_name);
+ WeakAliases_fini(&ob->weak_aliases);
obj_image_free_(ob);
ob->heap->free(ob->heap, ob, sizeof(*ob));
}
@@ -191,6 +207,46 @@ int obj_get_coff_import_name(const ObjBuilder* ob, Sym* out) {
return 1;
}
+void obj_set_weak_alias(ObjBuilder* ob, ObjSymId sym, Sym target) {
+ if (!ob || sym == OBJ_SYM_NONE || target == 0) return;
+ /* Overwrite an existing entry for this sym rather than duplicating. */
+ u32 n = WeakAliases_count(&ob->weak_aliases);
+ for (u32 i = 0; i < n; ++i) {
+ ObjWeakAlias* a = WeakAliases_at(&ob->weak_aliases, i);
+ if (a->sym == sym) {
+ a->target = target;
+ return;
+ }
+ }
+ ObjWeakAlias* slot = WeakAliases_push(&ob->weak_aliases, NULL);
+ if (!slot) return; /* OOM: alias recovery falls back to the name heuristic */
+ slot->sym = sym;
+ slot->target = target;
+}
+
+Sym obj_get_weak_alias(const ObjBuilder* ob, ObjSymId sym) {
+ if (!ob || sym == OBJ_SYM_NONE) return 0;
+ u32 n = WeakAliases_count(&ob->weak_aliases);
+ for (u32 i = 0; i < n; ++i) {
+ const ObjWeakAlias* a = WeakAliases_at(&ob->weak_aliases, i);
+ if (a->sym == sym) return a->target;
+ }
+ return 0;
+}
+
+u32 obj_weak_alias_count(const ObjBuilder* ob) {
+ return ob ? WeakAliases_count(&ob->weak_aliases) : 0;
+}
+
+int obj_weak_alias_at(const ObjBuilder* ob, u32 i, ObjSymId* sym_out,
+ Sym* target_out) {
+ if (!ob || i >= WeakAliases_count(&ob->weak_aliases)) return 0;
+ const ObjWeakAlias* a = WeakAliases_at(&ob->weak_aliases, i);
+ if (sym_out) *sym_out = a->sym;
+ if (target_out) *target_out = a->target;
+ return 1;
+}
+
/* ---- linked-image view ---- */
struct ObjImage {
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -561,6 +561,21 @@ int obj_get_coff_import_dll(const ObjBuilder*, Sym* out);
void obj_set_coff_import_name(ObjBuilder*, Sym import_name);
int obj_get_coff_import_name(const ObjBuilder*, Sym* out);
+/* COFF WEAK_EXTERNAL alias: symbol `sym` is an alias for the symbol named
+ * `target` (the aux record's fall-back/default symbol). Recorded by read_coff
+ * for genuine alias declarations (IMAGE_WEAK_EXTERN_SEARCH_ALIAS) so the linker
+ * can resolve the weak symbol to its target by name — e.g. mingw x86_64's
+ * `_setjmp` aliasing `__intrinsic_setjmp`, a redirection the single-underscore
+ * naming heuristic can't derive. The getter returns 0 when `sym` has no
+ * recorded alias (the common case). See src/link/link_resolve.c. */
+void obj_set_weak_alias(ObjBuilder*, ObjSymId sym, Sym target);
+Sym obj_get_weak_alias(const ObjBuilder*, ObjSymId sym);
+/* Enumerate the recorded weak-external aliases (for building a cross-input
+ * name->target map at link time). Count is 0 on inputs that carry none. */
+u32 obj_weak_alias_count(const ObjBuilder*);
+int obj_weak_alias_at(const ObjBuilder*, u32 i, ObjSymId* sym_out,
+ Sym* target_out);
+
/* Per-symbol format-specific flag bits. ObjSym.flags is otherwise
* unused; readers stash format-specific attribute bits there so the
* matching emitter can re-apply them. Today this is Mach-O n_desc
diff --git a/test/link/coff_weak_alias_test.c b/test/link/coff_weak_alias_test.c
@@ -0,0 +1,162 @@
+/* COFF WEAK_EXTERNAL alias capture in read_coff.
+ *
+ * mingw's x86_64 <setjmp.h> expands setjmp() to the intrinsic `_setjmp`, which
+ * libucrt.a provides as a COFF weak-external ALIAS to `__intrinsic_setjmp` (a
+ * DLL short-import). kit used to drop the aux TagIndex and lean on a single-
+ * underscore naming heuristic that can't derive `__intrinsic_setjmp` from
+ * `_setjmp`, so the cross-link failed with `undefined reference to '_setjmp'`.
+ * read_coff now records the alias target (when Characteristics is
+ * IMAGE_WEAK_EXTERN_SEARCH_ALIAS) so link_resolve can follow it by name.
+ *
+ * This pins the read-side decode: hand-build a minimal AMD64 COFF object whose
+ * symbol table carries the `_setjmp` -> `__intrinsic_setjmp` alias declarator,
+ * read it, and assert obj_get_weak_alias recovers the target. A negative case
+ * (a plain weak external with the SEARCH_LIBRARY policy kit's own emitter uses)
+ * confirms non-alias weak symbols record nothing.
+ *
+ * Exit 0 = pass; non-zero = fail. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <kit/core.h>
+
+#include "core/core.h"
+#include "core/pool.h"
+#include "lib/kit_unit.h"
+#include "obj/obj.h"
+
+static KitUnit g_u;
+#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__)
+
+#define IMAGE_FILE_MACHINE_AMD64 0x8664u
+#define SC_EXTERNAL 2u
+#define SC_WEAK_EXTERNAL 105u
+#define SEARCH_LIBRARY 2u
+#define SEARCH_ALIAS 3u
+
+static void wr16(u8* p, u32 v) {
+ p[0] = (u8)v;
+ p[1] = (u8)(v >> 8);
+}
+static void wr32(u8* p, u32 v) {
+ p[0] = (u8)v;
+ p[1] = (u8)(v >> 8);
+ p[2] = (u8)(v >> 16);
+ p[3] = (u8)(v >> 24);
+}
+
+/* Build a 3-record symbol-table COFF object (no sections):
+ * idx 0: `__intrinsic_setjmp` (undef EXTERNAL, long name via strtab)
+ * idx 1: `weak_name` (WEAK_EXTERNAL, short name, 1 aux)
+ * idx 2: aux -> TagIndex 0 (`__intrinsic_setjmp`), `characteristics`
+ * `weak_name` must be <= 8 bytes (short name). Returns the byte length. */
+static size_t build_coff(u8* out, const char* weak_name, u32 characteristics) {
+ static const char kTarget[] = "__intrinsic_setjmp";
+ const u32 nsyms = 3;
+ const u32 symtab_off = 20;
+ const u32 strtab_off = symtab_off + nsyms * 18u;
+ /* string table: 4-byte inclusive size + the target name + NUL */
+ const u32 target_len = (u32)sizeof kTarget; /* includes NUL */
+ const u32 strtab_size = 4u + target_len;
+
+ memset(out, 0, strtab_off + strtab_size);
+
+ /* ---- file header ---- */
+ wr16(out + 0, IMAGE_FILE_MACHINE_AMD64); /* Machine */
+ wr16(out + 2, 0); /* NumberOfSections */
+ wr32(out + 4, 0); /* TimeDateStamp */
+ wr32(out + 8, symtab_off); /* PointerToSymbolTable */
+ wr32(out + 12, nsyms); /* NumberOfSymbols */
+ wr16(out + 16, 0); /* SizeOfOptionalHeader */
+ wr16(out + 18, 0); /* Characteristics */
+
+ /* ---- symbol 0: __intrinsic_setjmp (long name) ---- */
+ u8* s0 = out + symtab_off;
+ wr32(s0 + 0, 0); /* long-name marker */
+ wr32(s0 + 4, 4); /* strtab offset (strings start at strtab+4) */
+ wr32(s0 + 8, 0);
+ wr16(s0 + 12, 0); /* SectionNumber = UNDEFINED */
+ wr16(s0 + 14, 0); /* Type */
+ s0[16] = (u8)SC_EXTERNAL;
+ s0[17] = 0; /* naux */
+
+ /* ---- symbol 1: weak_name (short name, weak external) ---- */
+ u8* s1 = out + symtab_off + 18u;
+ size_t wn = strlen(weak_name);
+ memcpy(s1, weak_name, wn); /* zero-padded to 8 by the memset above */
+ wr32(s1 + 8, 0);
+ wr16(s1 + 12, 0); /* SectionNumber = UNDEFINED */
+ wr16(s1 + 14, 0);
+ s1[16] = (u8)SC_WEAK_EXTERNAL;
+ s1[17] = 1; /* one aux record */
+
+ /* ---- symbol 2: weak-external aux ---- */
+ u8* aux = out + symtab_off + 36u;
+ wr32(aux + 0, 0); /* TagIndex -> symbol 0 */
+ wr32(aux + 4, characteristics); /* IMAGE_WEAK_EXTERN_SEARCH_* */
+
+ /* ---- string table ---- */
+ u8* st = out + strtab_off;
+ wr32(st + 0, strtab_size);
+ memcpy(st + 4, kTarget, target_len);
+
+ return strtab_off + strtab_size;
+}
+
+static Sym intern(Compiler* c, const char* s) {
+ return pool_intern_slice(c->global,
+ (Slice){.s = s, .len = (u32)strlen(s)});
+}
+
+int main(void) {
+ KitCompiler* kc = NULL;
+ u8 buf[160];
+ kit_unit_init(&g_u);
+
+ KitTargetSpec t =
+ kit_unit_target(KIT_ARCH_X86_64, KIT_OS_WINDOWS, KIT_OBJ_COFF);
+ if (kit_unit_compiler_new(&g_u, t, &kc) != KIT_OK || !kc) {
+ fprintf(stderr, "compiler_new failed\n");
+ return 2;
+ }
+ Compiler* c = kc; /* KitCompiler IS the internal Compiler */
+
+ /* Positive: a genuine alias declaration records its target. */
+ {
+ size_t len = build_coff(buf, "_setjmp", SEARCH_ALIAS);
+ ObjBuilder* ob = read_coff(c, "weak_alias.o", buf, len);
+ EXPECT(ob != NULL, "read_coff returned NULL");
+ if (ob) {
+ ObjSymId id = obj_symbol_find(ob, intern(c, "_setjmp"));
+ EXPECT(id != OBJ_SYM_NONE, "_setjmp symbol not found after read");
+ Sym target = obj_get_weak_alias(ob, id);
+ EXPECT(target != 0, "alias target not recorded for _setjmp");
+ if (target != 0) {
+ Slice s = pool_slice(c->global, target);
+ EXPECT(s.len == strlen("__intrinsic_setjmp") &&
+ memcmp(s.s, "__intrinsic_setjmp", s.len) == 0,
+ "alias target = '%.*s', want '__intrinsic_setjmp'", (int)s.len,
+ s.s);
+ }
+ obj_free(ob);
+ }
+ }
+
+ /* Negative: SEARCH_LIBRARY (kit's own STB_WEAK emit shape) is not an alias. */
+ {
+ size_t len = build_coff(buf, "wlib", SEARCH_LIBRARY);
+ ObjBuilder* ob = read_coff(c, "weak_lib.o", buf, len);
+ EXPECT(ob != NULL, "read_coff (lib) returned NULL");
+ if (ob) {
+ ObjSymId id = obj_symbol_find(ob, intern(c, "wlib"));
+ EXPECT(id != OBJ_SYM_NONE, "wlib symbol not found after read");
+ EXPECT(obj_get_weak_alias(ob, id) == 0,
+ "SEARCH_LIBRARY weak should not record an alias target");
+ obj_free(ob);
+ }
+ }
+
+ kit_unit_summary(&g_u, "coff_weak_alias_test");
+ return kit_unit_status(&g_u);
+}