kit

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

commit e51cf4693982cac804e78807d5e31a9293b67a66
parent d1e302e28a2ae299d6e5beeec54d80ee0ec6087b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  9 Jun 2026 12:11:16 -0700

windows: resolve JIT printf-family and thread-local gaps

Two things an AOT `kit cc` link resolved but the in-process JIT (`kit run`)
did not on Windows. Both are now fixed and VM-verified on aarch64-windows;
the JIT Toy corpus goes 164/166 -> 166/166.

printf-family: with kit's UCRT profile (__USE_MINGW_ANSI_STDIO=0) mingw's
<stdio.h> resolves the printf/scanf family to out-of-line wrappers that live
only in libucrt.a -- ucrtbase.dll exports just the __stdio_common_v* cores --
so the JIT (which resolves externs via dlsym over loaded DLLs, no static
archives) left `printf` undefined. driver_dlsym_resolver now hands JIT'd code
kit.exe's own statically-linked copies of the family via a KIT_STATIC_STDIO
macro (taking each address pulls its libucrt.a definition into our image).
DLL-exported entry points (puts/fputs/fflush/__stdio_common_v*/__acrt_iob_func)
still resolve through win_dlsym.

thread-local local-exec: the COFF TLS-access idiom (TEB ThreadLocalStoragePointer
-> _tls_index -> block -> +SECREL) cannot run under the JIT (no loaded PE for a
_tls_index, no per-module TEB block). As with ELF Local-Exec, the single-threaded
JIT now relaxes the idiom to address the in-image .tls instance directly: a
unified jit_reloc_is_tls_le classifier routes the terminal SECREL (aa64 SECREL12A
kinds; x64 SECREL to an SK_TLS target) to the per-arch jit_tls_le_relax, which
collapses the 4-/7-instruction sequence to `lea rd,[rip+&var]` / nops+adrp+add;
_tls_index is treated as a JIT-defined no-op (resolve + append preflight) and its
idiom relocs are dropped. 141/142 now return 43/134 under JIT, and a C
_Thread_local program mutates/reads correctly.

test/link/jit_tls_relax_test.c pins the byte rewrite for both arches (the only
automated coverage for the x86-64 idiom, which can't run end-to-end until the
x64 self-host _setjmp blocker is closed); the x64 idiom builder is pinned to a
real-disasm golden. doc/plan/windows.md: mark the gaps done and collect the
remaining open bugs (the AOT "malformed UCN" rt-build defect and the x64
_setjmp cross-link blocker).

Diffstat:
Mdoc/plan/windows.md | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mdriver/env/windows.c | 56++++++++++++++++++++++++++++++++++++++++++--------------
Mmk/test.mk | 11+++++++++++
Mmk/test_unit.mk | 3++-
Msrc/arch/aa64/reloc.c | 39++++++++++++++++++++++++++++++++++-----
Msrc/arch/x64/reloc.c | 49+++++++++++++++++++++++++++++++++++++------------
Msrc/link/link_jit.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/link/link_resolve.c | 11+++++++++++
Atest/link/jit_tls_relax_test.c | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 494 insertions(+), 73 deletions(-)

diff --git a/doc/plan/windows.md b/doc/plan/windows.md @@ -34,8 +34,9 @@ follow-on (rt path resolution). - **Toy corpus on the VM** (native `kit.exe`, compare `.expected`): - AOT (`kit cc` compile + link + execute): **166 / 166 pass**, 0 miscompile, 0 crash, at `-O0`. - - JIT (`kit run`): **164 / 166** — the two misses are the thread-local cases - (§1), which return a clean error, not a crash. + - JIT (`kit run`): **166 / 166** — the printf-family and thread-local gaps + (§1) are fixed; the two thread-local cases (`141`, `142`) now return their + values (43, 134). Fixes the bring-up required (all in tree): - **Large-frame stack probes in codegen** — *the fix that closed the ~17 @@ -75,35 +76,80 @@ Fixes the bring-up required (all in tree): `GetModuleFileNameW`) when `argv[0]` isn't a usable path (PATH invocation), so `<bindir>/support/rt` resolves from any cwd. -The items below are what is **not** yet done. - -## 1. JIT runtime gaps: printf-family and thread-locals - -Two things an AOT `kit cc` link resolves but the in-process JIT (`kit run`) does -not, on Windows specifically. Neither is a codegen bug — the same programs -compile+link+run correctly under AOT, and run under the host JIT on -macOS/Linux. - -**printf-family.** `kit run` of a printf program fails: `undefined reference to -'printf'`. mingw's `<stdio.h>` printf wrapper resolves to the static `printf` -(and `__local_stdio_printf_options`) that live only in `libucrt.a` — not DLL -exports — and the JIT links no static archives (it resolves externs via dlsym -over loaded DLLs + in-image symbols). AOT `kit cc` printf works fully (it links -`libucrt.a`); `puts` and other direct ucrtbase exports work under JIT. - -Options: (a) feed the hosted profile's static archives to the JIT link so it -lazily pulls static members like `printf`/`__local_stdio_printf_options` (and -routes the short-import members to dlsym) — the general fix; or (b) extend the -`driver/env/windows.c` dlsym fallback (already used for the -`__local_stdio_*_options` helpers) to the small set of libucrt-static stdio -entry points, handing JIT'd code kit.exe's own statically-linked copies. - -**Thread-local local-exec.** `kit run` of a `threadlocal`-using program returns -a clean error (rc 1), not the program's value — local-exec TLS isn't resolved -by the in-process JIT on Windows. The same cases compile+link+run correctly via -AOT `kit cc` (PE TLS directory) and under the host JIT, so it's a Windows-JIT -TLS-resolution gap. Repro: `141_threadlocal_mutate` (expect 43), -`142_threadlocal_multi` (expect 134) — the only two Toy JIT misses. +The items below are what is **not** yet done. The concrete open bugs/blockers +are collected first; the numbered sections are the larger roadmap items. + +## Open bugs / blockers + +Concrete defects surfaced during bring-up, each blocking a roadmap item below. + +- **AOT `malformed UCN` building rt on the VM** (blocks native `kit cc`'s + on-demand rt path, and §5 bootstrap). On the VM, `kit.exe cc <prog>` fails + while compiling the runtime: + `support/rt/lib/int/int.c:203:46: fatal: malformed UCN`. That line is + `if (a == ((di_int)((du_int)1 << (N - 1)))) compilerrt_abort();` — there is no + `\u`/`\U` anywhere near column 46, so "malformed UCN" is a *misdiagnosis*: the + Windows-hosted lexer is mis-tokenizing valid source. It is **not** a stale + deploy — it reproduces with a freshly-shipped rt tree, and with the *baseline* + `kit.exe` (predating the JIT-gap work), so it is pre-existing. The macOS host + `build/kit` compiles the *same* `int.c` for `aarch64-windows` cleanly + (`make rt-aarch64-windows` succeeds), so the bug is specific to the + **Windows-hosted** kit.exe. Leading hypotheses: (a) a Windows-only file-read / + line-ending / encoding path feeding the lexer; or (b) a latent + **self-compile codegen bug** in the cross-built kit.exe that only manifests in + the lexer's UCN/escape handling — which would also threaten §5 (a stage-2 + kit.exe that miscompiles its own lexer). Worth bisecting against a known-good + source byte stream on the VM (e.g. `kit cc` a minimal file with the exact + bytes of line 203) to separate (a) from (b). *Workaround used for verifying + the JIT fixes:* host-cross-compile the `.exe` on macOS, then run it on the VM, + which never invokes the VM-side rt build. + +- **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. + +## 1. JIT runtime gaps: printf-family and thread-locals — DONE + +Two things an AOT `kit cc` link resolved but the in-process JIT (`kit run`) did +not, on Windows specifically. Both are now fixed and VM-verified on +aarch64-windows; neither was a codegen bug. + +**printf-family (option b).** `kit run` of a printf program failed: `undefined +reference to 'printf'`. With kit's UCRT profile (`__USE_MINGW_ANSI_STDIO=0`) +mingw's `<stdio.h>` resolves the printf/scanf family to out-of-line wrappers +that live only in `libucrt.a` — ucrtbase.dll exports just the +`__stdio_common_v*` cores (and `__local_stdio_*` helpers). The JIT links no +static archives (it resolves externs via dlsym over loaded DLLs), so those +wrappers were unresolvable. Fix: `driver/env/windows.c`'s `driver_dlsym_resolver` +now hands JIT'd code kit.exe's own statically-linked copies of the family +(`printf`, `fprintf`, `sprintf`, `snprintf`/`_snprintf`, the `v*`/`scanf` +variants, …) — taking each function's address pulls its `libucrt.a` definition +into our image. DLL-exported entry points (`puts`, `fputs`, `fflush`, +`__stdio_common_v*`, `__acrt_iob_func`) still resolve through dlsym and never +reach the table. (Option (a) — feeding the JIT link the static archives — was +the more general alternative; option (b) is smaller and sufficient.) + +**Thread-local local-exec.** `kit run` of a `threadlocal`-using program returned +a clean error (rc 1) — the COFF TLS-access idiom (read TEB +ThreadLocalStoragePointer, index by `_tls_index`, then `+ SECREL(var)`) can't +run under the JIT: there is no loaded PE the OS could assign a `_tls_index`, nor +a per-module TLS block in the thread's TEB array. Fix: as with ELF Local-Exec +(`RELOC_IS_TLS_LE`), the single-threaded JIT now relaxes the idiom to address +the in-image `.tls` instance directly. The terminal SECREL reloc(s) drive a +per-arch idiom rewrite (`x64_jit_tls_le_relax` / `aa64_jit_tls_le_relax` collapse +the 4-/7-instruction sequence to `lea rd,[rip+&var]` / `adrp+add`), the +`_tls_index` symbol is treated as a JIT-defined no-op (so resolve doesn't reject +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). ## 2. x86_64-windows parity @@ -113,7 +159,11 @@ path isn't built/verified end to end: - 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. + 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` `*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`. @@ -162,7 +212,10 @@ now that the large-frame stack-probe crash is fixed — kit.exe compiles + links runs the full Toy AOT corpus (166/166) on the VM. Next: confirm kit.exe can compile the full libkit/driver source set on the VM, then add a `scripts/windows_bootstrap.sh` to drive the VM-side stages and the -stage-2==stage-3 byte-identity check. +stage-2==stage-3 byte-identity check. **Gated** by the `malformed UCN` bug (see +*Open bugs / blockers*): a stage-1 kit.exe that can't even compile the rt source +tree on the VM can't compile libkit there — and if that bug is a self-compile +codegen defect, it threatens stage-2==stage-3 identity directly. ## 6. Debugger fault-guard / SEH on Windows diff --git a/driver/env/windows.c b/driver/env/windows.c @@ -1207,28 +1207,56 @@ static void* win_dlsym(const char* name) { return NULL; } -/* mingw's <stdio.h> printf/scanf inline wrappers call these per-module option - * helpers, whose out-of-line definitions live ONLY in libucrt.a — no DLL - * exports them. AOT links them in; the JIT resolves externs via dlsym over - * loaded DLLs (win_dlsym), which therefore can't find them, leaving a - * printf-using JIT program with an undefined __local_stdio_printf_options. - * kit.exe statically links libucrt.a, and referencing these here pulls their - * definitions into our own image, so we can hand JIT'd code our copies. The - * options storage only ever holds the default (0) flags, so sharing it between - * the host and the JIT'd program is benign. */ static int win_name_eq(KitSlice s, const char* lit, size_t litlen) { return s.len == litlen && memcmp(s.s, lit, litlen) == 0; } +/* libucrt-static stdio entry points handed to JIT'd code. + * + * With kit's UCRT profile (__USE_MINGW_ANSI_STDIO=0), mingw's <stdio.h> resolves + * the printf/scanf family to out-of-line wrappers that live ONLY in libucrt.a — + * ucrtbase.dll exports just the __stdio_common_v* cores (and __local_stdio_* + * helpers the wrappers call). An AOT `kit cc` link pulls the wrappers from + * libucrt.a; the in-process JIT (`kit run`) resolves externs via dlsym over + * loaded DLLs (win_dlsym), which can't see them, so a printf-using JIT program + * fails with an undefined `printf`. kit.exe statically links libucrt.a, so + * taking each wrapper's address here pulls its definition into our own image and + * lets us hand the JIT our copy. Functions that ARE ucrtbase.dll exports (puts, + * fputs, fflush, putchar, __stdio_common_v*, __acrt_iob_func, …) resolve through + * win_dlsym above and never reach this table. The __local_stdio_*_options + * helpers' storage only ever holds the default (0) flags, so sharing one + * instance between the host and the JIT'd program is benign. */ void* driver_dlsym_resolver(void* user, KitSlice name_s) { + void* p; (void)user; if (!name_s.s || name_s.len == 0) return NULL; - void* p = win_dlsym(name_s.s); + p = win_dlsym(name_s.s); if (p) return p; - if (win_name_eq(name_s, "__local_stdio_printf_options", 28)) - return (void*)(uintptr_t)&__local_stdio_printf_options; - if (win_name_eq(name_s, "__local_stdio_scanf_options", 27)) - return (void*)(uintptr_t)&__local_stdio_scanf_options; + /* Match by source name; #fn stringizes it and sizeof-1 is its length. */ +#define KIT_STATIC_STDIO(fn) \ + if (win_name_eq(name_s, #fn, sizeof(#fn) - 1u)) return (void*)(uintptr_t)&fn + KIT_STATIC_STDIO(__local_stdio_printf_options); + KIT_STATIC_STDIO(__local_stdio_scanf_options); + KIT_STATIC_STDIO(printf); + KIT_STATIC_STDIO(fprintf); + KIT_STATIC_STDIO(sprintf); + KIT_STATIC_STDIO(snprintf); + KIT_STATIC_STDIO(_snprintf); + KIT_STATIC_STDIO(vprintf); + KIT_STATIC_STDIO(vfprintf); + KIT_STATIC_STDIO(vsprintf); + KIT_STATIC_STDIO(vsnprintf); + KIT_STATIC_STDIO(_vsnprintf); + KIT_STATIC_STDIO(scanf); + KIT_STATIC_STDIO(fscanf); + KIT_STATIC_STDIO(sscanf); + KIT_STATIC_STDIO(_snscanf); + KIT_STATIC_STDIO(vscanf); + KIT_STATIC_STDIO(vfscanf); + KIT_STATIC_STDIO(vsscanf); + KIT_STATIC_STDIO(_scprintf); + KIT_STATIC_STDIO(_vscprintf); +#undef KIT_STATIC_STDIO return NULL; } diff --git a/mk/test.mk b/mk/test.mk @@ -96,6 +96,7 @@ TEST_TARGETS = \ test-link-reloc-uleb128 \ test-link-reloc-desc \ test-link-reloc-apply \ + test-link-jit-tls-relax \ test-macho \ test-native-direct-target \ test-opt \ @@ -163,6 +164,7 @@ DEFAULT_TEST_TARGETS = \ test-link-reloc-uleb128 \ test-link-reloc-desc \ test-link-reloc-apply \ + test-link-jit-tls-relax \ test-dbg \ test-disasm-complete \ test-macho \ @@ -394,6 +396,15 @@ RELOC_APPLY_TEST_BIN = build/test/reloc_apply_test test-link-reloc-apply: $(RELOC_APPLY_TEST_BIN) $(RELOC_APPLY_TEST_BIN) +# In-process JIT TLS Local-Exec relaxation for the Windows (COFF) TEB idiom: +# pins the per-arch byte rewrite (jit_tls_le_relax) for x86-64 and aarch64. The +# aarch64 path is also covered end-to-end on the Windows VM; this is the only +# automated guard for the x86-64 idiom. Internal-surface (raw lib objects). +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) + # 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 \ + interp_smoke_test jit_tls_relax_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 @@ -67,6 +67,7 @@ aa64_sweep_gen_SRC := test/arch/aa64_sweep_gen.c 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 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/src/arch/aa64/reloc.c b/src/arch/aa64/reloc.c @@ -223,19 +223,48 @@ int aa64_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, } /* In-process JIT TLS Local-Exec relaxation (LinkArchDesc.jit_tls_le_relax). - * Codegen emits, per access: + * + * ELF, per access: * mrs rd, tpidr_el0 (no reloc) * add rd, rd, #hi12 R_AARCH64_TLSLE_ADD_TPREL_HI12 <- `site` * add rd, rd, #lo12 R_AARCH64_TLSLE_ADD_TPREL_LO12_NC - * Single-threaded JIT: address the in-image storage directly, dropping the - * thread-pointer read: - * adrp rd, &var ; add rd, rd, :lo12:&var ; nop - * The HI12 reloc drives the whole rewrite; the LO12 half is then a no-op. */ + * + * Windows/COFF, per access — the 7-instruction TEB idiom (see + * aa_tls_addr_of_win): + * site-20 ldr rd, [x18, #0x58] TEB.ThreadLocalStoragePointer + * site-16 adrp x16, _tls_index } + * site-12 add x16, x16, :lo12:_tls_index } &_tls_index (relocs dropped) + * site-8 ldr w16, [x16] module TLS index + * site-4 ldr rd, [rd, x16, lsl #3] this module's TLS block base + * site add rd, rd, :secrel_hi12:sym R_COFF_AARCH64_SECREL_HIGH12A <- + * site+4 add rd, rd, :secrel_lo12:sym R_COFF_AARCH64_SECREL_LOW12A + * + * Single-threaded JIT: in both cases address the in-image storage directly, + * dropping the thread-pointer read (and, on Windows, the `_tls_index` / TEB + * indirection): adrp rd, &var ; add rd, rd, :lo12:&var ; nop(s). */ void aa64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, u64 site_pc) { u8* mrs; u8* add_lo; u32 rd; + /* Windows COFF idiom: the terminal HIGH12A drives the whole rewrite; the + * LOW12A half is then a no-op (mirrors the ELF HI12/LO12 split). */ + if (k == R_COFF_AARCH64_SECREL_LOW12A) return; /* handled with HIGH12A */ + if (k == R_COFF_AARCH64_SECREL_HIGH12A) { + u8* p; + rd = rd_u32_le(site) & 0x1fu; + /* nop the TEB read, the _tls_index materialize + load, and the block load + * (site-20 .. site-8); reuse the block-load slot at site-4 for the ADRP. */ + for (p = site - 20; p <= site - 8; p += 4) wr_u32_le(p, 0xd503201fu); + wr_u32_le(site - 4, 0x90000000u | rd); /* adrp rd, #0 */ + aa64_reloc_apply_insn(c, R_AARCH64_ADR_PREL_PG_HI21, site - 4, storage, 0, + site_pc - 4u); + wr_u32_le(site, 0x91000000u | (rd << 5) | rd); /* add rd, rd, #0 */ + aa64_reloc_apply_insn(c, R_AARCH64_ADD_ABS_LO12_NC, site, storage, 0, + site_pc); + wr_u32_le(site + 4, 0xd503201fu); /* nop the secrel_lo12 add */ + return; + } if (k == R_AARCH64_TLSLE_ADD_TPREL_LO12_NC) return; /* handled with HI12 */ if (k != R_AARCH64_TLSLE_ADD_TPREL_HI12) compiler_panic(c, SRCLOC_NONE, "aa64 jit tls: unexpected reloc kind %u", diff --git a/src/arch/x64/reloc.c b/src/arch/x64/reloc.c @@ -59,12 +59,24 @@ int x64_reloc_apply_insn(Compiler* c, RelocKind k, u8* P_bytes, u64 S, i64 A, } /* In-process JIT TLS Local-Exec relaxation (LinkArchDesc.jit_tls_le_relax). - * Codegen emits, per access: + * + * ELF (R_X64_TPOFF32), per access: * 64 REX.W 8B modrm sib disp32 mov rd, fs:[0] (fixed 9 bytes) * REX.W 8D modrm [sib] disp32 lea rd,[rd+tpoff] (7 or 8 bytes; `site` - * points at this disp32, R_X64_TPOFF32) - * Single-threaded JIT: nop the whole block and emit `lea rd,[rip+&var]` so rd - * holds the in-image storage address, dropping the fs read. */ + * points at this disp32) + * + * Windows/COFF (R_COFF_SECREL to a TLS symbol), per access — the 4-instruction + * TEB idiom (see x64_tls_addr_of_win64), all heading the trailing lea: + * 65 REX.W 8B modrm sib disp32 mov rd, gs:[0x58] (9 bytes) + * 44 8B 1D disp32 mov r11d, [rip+_tls_index] (7 bytes) + * REX 8B modrm sib [disp8] mov rd, [rd+r11*8] (4 or 5 bytes) + * REX.W 8D modrm [sib] disp32 lea rd,[rd+sym@SECREL] (7 or 8 bytes; + * `site` points at this disp32) + * + * Single-threaded JIT: in both cases nop the whole block and emit + * `lea rd,[rip+&var]` so rd holds the in-image storage address, dropping the + * segment read (and, on Windows, the `_tls_index` / TEB indirection — its + * `_tls_index` reloc is separately dropped by the JIT reloc pass). */ void x64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, u64 site_pc) { u8* block_end = site + 4; /* disp32 ends the lea */ @@ -73,16 +85,29 @@ void x64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, u8* p; u32 rd; i64 disp; - if (k != R_X64_TPOFF32) + if (k == R_X64_TPOFF32) { + /* The 9-byte fs-mov ends where the lea (7 or 8 bytes) begins. */ + p = block_end - 7 - 9; + if (p[0] == 0x64u && p[2] == 0x8Bu) + mov = p; + else { + p = block_end - 8 - 9; + if (p[0] == 0x64u && p[2] == 0x8Bu) mov = p; + } + } else if (k == R_COFF_SECREL) { + /* The gs-mov heads the idiom; its distance to block_end is + * 9 + 7 + (4|5) + (7|8) = 27..29 bytes. Match the gs-mov's strong + * signature: 65 <rex> 8B <modrm> 25 58 00 00 00. */ + int off; + for (off = 27; off <= 29 && !mov; ++off) { + p = block_end - off; + if (p[0] == 0x65u && p[2] == 0x8Bu && p[4] == 0x25u && p[5] == 0x58u && + p[6] == 0u && p[7] == 0u && p[8] == 0u) + mov = p; + } + } else { compiler_panic(c, SRCLOC_NONE, "x64 jit tls: unexpected reloc kind %u", (unsigned)k); - /* The 9-byte fs-mov ends where the lea (7 or 8 bytes) begins. */ - p = block_end - 7 - 9; - if (p[0] == 0x64u && p[2] == 0x8Bu) - mov = p; - else { - p = block_end - 8 - 9; - if (p[0] == 0x64u && p[2] == 0x8Bu) mov = p; } if (!mov) compiler_panic(c, SRCLOC_NONE, "x64 jit tls: unexpected access sequence"); diff --git a/src/link/link_jit.c b/src/link/link_jit.c @@ -121,6 +121,40 @@ static int perms_for(u32 secflags) { return p; } +/* Windows (COFF) Local-Exec TLS in the single-threaded in-process JIT. + * + * The codegen emits the PE TLS-access idiom: read TEB.ThreadLocalStoragePointer, + * index it by the module's `_tls_index`, then `+ SECREL(var)`. That cannot run + * under the JIT — there is no loaded PE the OS could assign a `_tls_index` to, + * nor a per-module TLS block in this thread's TEB array. As with ELF Local-Exec + * (RELOC_IS_TLS_LE), the JIT relaxes the idiom to address the single in-image + * .tls instance directly: the terminal SECREL reloc(s) drive the relaxation + * (jit_tls_le_relax), and the idiom's `_tls_index` ADRP/PC32 relocs are dropped + * because those instructions get rewritten away. */ +static int jit_tls_is_index_ref(Compiler* c, const LinkSymbol* tgt) { + Slice nm; + if (obj_format_tls_model(c) != OBJ_TLS_WINDOWS_TEB) return 0; + if (!tgt || tgt->name == 0) return 0; + nm = pool_slice(c->global, tgt->name); + return nm.len == 10u && memcmp(nm.s, "_tls_index", 10u) == 0; +} + +/* True for the terminal SECREL reloc(s) of the Windows TLS idiom: aarch64's + * SECREL12A kinds are TLS-only, while x86-64's generic SECREL is a TLS access + * exactly when its target is a TLS symbol. */ +static int jit_reloc_is_win_tls_le(Compiler* c, RelocKind k, u8 tgt_kind) { + if (obj_format_tls_model(c) != OBJ_TLS_WINDOWS_TEB) return 0; + return k == R_COFF_AARCH64_SECREL_HIGH12A || + k == R_COFF_AARCH64_SECREL_LOW12A || + (k == R_COFF_SECREL && tgt_kind == SK_TLS); +} + +/* Unified TLS Local-Exec classifier for the JIT reloc passes: the arch-neutral + * RELOC_IS_TLS_LE flag (ELF) plus the COFF Windows idiom above. */ +static int jit_reloc_is_tls_le(Compiler* c, RelocKind k, u8 tgt_kind) { + return reloc_kind_is_tls_le(c, k) || jit_reloc_is_win_tls_le(c, k, tgt_kind); +} + /* Find the segment that contains image-relative `vaddr` and return its * runtime address (the runtime alias, not the write alias). Up to 3 * segments after layout, so a linear scan is fine. @@ -359,10 +393,14 @@ KitJit* kit_jit_from_image(LinkImage* img) { const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1); u64 S, P; u8* P_bytes; - /* ELF Local-Exec TLS -> in-image addressing (single-threaded JIT). The - * per-arch idiom rewrite lives behind LinkArchDesc.jit_tls_le_relax; this - * loop stays arch-neutral, classifying via the RELOC_IS_TLS_LE flag. */ - if (reloc_kind_is_tls_le(c, r->kind)) { + /* Windows TLS idiom `_tls_index` references are dropped: the access is + * relaxed to in-image addressing, so the index load is rewritten away. */ + if (jit_tls_is_index_ref(c, tgt)) continue; + /* Local-Exec TLS -> in-image addressing (single-threaded JIT). The per-arch + * idiom rewrite lives behind LinkArchDesc.jit_tls_le_relax; this loop stays + * arch-neutral, classifying ELF via RELOC_IS_TLS_LE and the COFF Windows + * idiom via its terminal SECREL reloc(s). */ + if (jit_reloc_is_tls_le(c, r->kind, tgt->kind)) { const LinkArchDesc* d = link_arch_desc_for(c); u64 storage = (tgt->kind == SK_ABS) ? tgt->vaddr + (u64)r->addend @@ -702,10 +740,12 @@ static void jit_apply_one_reloc(KitJit* jit, const LinkRelocApply* r) { u64 S; u64 P; u8* P_bytes; - /* ELF Local-Exec TLS -> in-image addressing (single-threaded JIT); arch - * idiom rewrite behind LinkArchDesc.jit_tls_le_relax (see kit_jit_from_image - * for the mirror of this in the initial reloc pass). */ - if (reloc_kind_is_tls_le(jit->c, r->kind)) { + /* Windows TLS idiom `_tls_index` references are dropped (see kit_jit_from_image). */ + if (jit_tls_is_index_ref(jit->c, tgt)) return; + /* Local-Exec TLS -> in-image addressing (single-threaded JIT); arch idiom + * rewrite behind LinkArchDesc.jit_tls_le_relax (see kit_jit_from_image for + * the mirror of this in the initial reloc pass). */ + if (jit_reloc_is_tls_le(jit->c, r->kind, tgt->kind)) { const LinkArchDesc* d = link_arch_desc_for(jit->c); u64 storage = (tgt->kind == SK_ABS) ? tgt->vaddr + (u64)r->addend @@ -854,6 +894,10 @@ static void jit_append_obj_inner(KitJit* jit, ObjBuilder* ob) { size_t nlen = nm_s.len; if (nm && nlen == 15u && memcmp(nm, "__tlv_bootstrap", 15u) == 0) ok = 1; + /* Windows COFF TLS module-index symbol: the JIT relaxes every TLS + * access to in-image addressing, so `_tls_index` is never read. */ + if (nm && nlen == 10u && memcmp(nm, "_tls_index", 10u) == 0) + ok = 1; if (!ok) { obj_format_demangle_c(jit->c, &nm, &nlen); obj_symiter_free(it); diff --git a/src/link/link_resolve.c b/src/link/link_resolve.c @@ -481,6 +481,17 @@ void link_resolve_undefs(Linker* l, LinkImage* img) { s->defined = 1; continue; } + /* Windows COFF Local-Exec TLS: the in-process JIT relaxes every TLS + * access to in-image addressing (see kit_jit_from_image), so the PE + * module-index symbol `_tls_index` — normally supplied by the OS loader + * via the TLS directory — is never read. Define it as 0 so resolve does + * not reject it; its idiom relocs are dropped in the JIT reloc pass. */ + if (nm && nlen == 10u && memcmp(nm, "_tls_index", 10u) == 0) { + s->kind = SK_ABS; + s->vaddr = 0; + s->defined = 1; + continue; + } } { Slice nm_s = s->name ? pool_slice(l->c->global, s->name) : SLICE_NULL; diff --git a/test/link/jit_tls_relax_test.c b/test/link/jit_tls_relax_test.c @@ -0,0 +1,219 @@ +/* In-process JIT TLS Local-Exec relaxation for the Windows (COFF) TEB idiom. + * + * The single-threaded JIT rewrites the PE TLS-access idiom (read TEB + * ThreadLocalStoragePointer, index by `_tls_index`, then `+ SECREL(var)`) to + * address the in-image .tls instance directly, dropping the TEB/`_tls_index` + * indirection (see src/link/link_jit.c and the per-arch jit_tls_le_relax). The + * aarch64 path is verified end-to-end on the Windows VM; this pins the + * byte-level rewrite for BOTH arches, and is the only automated guard for the + * x86-64 idiom (x64-windows self-host can't run end-to-end yet). + * + * Each case hand-encodes the exact idiom the codegen emits + * (x64_tls_addr_of_win64 / aa_tls_addr_of_win), drives the relax, and asserts + * the result is the in-image address materialization. The x64 builder is pinned + * to a golden captured from real `kit cc -c` disassembly so it can't drift away + * from the codegen it must match. + * + * Exit 0 = pass; non-zero = fail. */ + +#include <stdint.h> +#include <string.h> + +#include <kit/core.h> + +#include "core/core.h" +#include "lib/kit_unit.h" +#include "obj/obj.h" + +/* Reached via LinkArchDesc.jit_tls_le_relax; not exposed in a header. */ +void x64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, + u64 site_pc); +void aa64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, + u64 site_pc); + +static KitUnit g_u; +#define EXPECT(cond, ...) CU_EXPECT(&g_u, cond, __VA_ARGS__) + +static KitCompiler* compiler_for(KitArchKind arch) { + static KitCompiler* aa64 = NULL; + static KitCompiler* x64 = NULL; + KitCompiler** slot = arch == KIT_ARCH_ARM_64 ? &aa64 : &x64; + if (!*slot) { + /* Windows/COFF so the compiler matches the idiom's origin; only the panic + * path reads it, so the exact spec barely matters. */ + KitTargetSpec t = kit_unit_target(arch, KIT_OS_WINDOWS, KIT_OBJ_COFF); + if (kit_unit_compiler_new(&g_u, t, slot) != KIT_OK || !*slot) { + fprintf(stderr, "compiler_new failed for arch=%d\n", (int)arch); + exit(2); + } + } + return *slot; +} + +static u32 rd32(const u8* p) { + return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24); +} + +/* ---- x86-64 -------------------------------------------------------------- */ + +/* Emit the 4-instruction Win64 TLS idiom for destination register `rd` into + * `b`, mirroring x64_tls_addr_of_win64 byte-for-byte. Returns total length; + * *site_off receives the offset of the trailing lea's SECREL disp32. */ +static u32 x64_emit_idiom(u8* b, u32 rd, u32* site_off) { + u32 n = 0; + /* (1) mov rd, gs:[0x58] (9 bytes) */ + b[n++] = 0x65; + b[n++] = (u8)(0x48u | ((rd & 8u) ? 0x04u : 0u)); + b[n++] = 0x8B; + b[n++] = (u8)(((rd & 7u) << 3) | 4u); + b[n++] = 0x25; + b[n++] = 0x58; + b[n++] = 0; + b[n++] = 0; + b[n++] = 0; + /* (2) mov r11d, [rip+_tls_index] (7 bytes) */ + b[n++] = 0x44; + b[n++] = 0x8B; + b[n++] = 0x1D; + b[n++] = 0; + b[n++] = 0; + b[n++] = 0; + b[n++] = 0; + /* (3) mov rd, [rd+r11*8] (4 or 5 bytes) */ + b[n++] = (u8)(0x4Au | ((rd & 8u) ? 0x05u : 0u)); + b[n++] = 0x8B; + if ((rd & 7u) == 5u) { + b[n++] = (u8)((1u << 6) | ((rd & 7u) << 3) | 4u); + b[n++] = (u8)(0xD8u | (rd & 7u)); + b[n++] = 0; + } else { + b[n++] = (u8)(((rd & 7u) << 3) | 4u); + b[n++] = (u8)(0xD8u | (rd & 7u)); + } + /* (4) lea rd, [rd + sym@SECREL] (7 or 8 bytes) */ + b[n++] = (u8)(0x48u | ((rd & 8u) ? 0x05u : 0u)); + b[n++] = 0x8D; + if ((rd & 7u) == 4u) { + b[n++] = (u8)((2u << 6) | ((rd & 7u) << 3) | 4u); + b[n++] = (u8)((4u << 3) | (rd & 7u)); + } else { + b[n++] = (u8)((2u << 6) | ((rd & 7u) << 3) | (rd & 7u)); + } + *site_off = n; + b[n++] = 0; + b[n++] = 0; + b[n++] = 0; + b[n++] = 0; + return n; +} + +static void x64_check(u32 rd) { + u8 buf[40]; + u32 site_off; + u32 total = x64_emit_idiom(buf, rd, &site_off); + /* Distinct write/runtime "addresses": disp must use site_pc, not &buf. */ + const u64 site_pc = 0x140005000ull + site_off; + const u64 storage = 0x140009123ull; + u32 lea = total - 7u; /* the rewritten 7-byte rip-lea sits at the block end */ + i64 want_disp = (i64)storage - (i64)(site_pc + 4u); + u32 i; + x64_jit_tls_le_relax(compiler_for(KIT_ARCH_X86_64), R_COFF_SECREL, + &buf[site_off], storage, site_pc); + for (i = 0; i < lea; ++i) + EXPECT(buf[i] == 0x90u, "x64 rd=%u: byte %u not NOP (0x%02x)", rd, i, + buf[i]); + EXPECT(buf[lea] == (u8)(0x48u | ((rd >= 8u) ? 0x04u : 0u)), + "x64 rd=%u: lea REX 0x%02x", rd, buf[lea]); + EXPECT(buf[lea + 1] == 0x8Du, "x64 rd=%u: lea opcode 0x%02x", rd, + buf[lea + 1]); + EXPECT(buf[lea + 2] == (u8)(((rd & 7u) << 3) | 5u), + "x64 rd=%u: lea modrm 0x%02x", rd, buf[lea + 2]); + EXPECT((i32)rd32(&buf[lea + 3]) == (i32)want_disp, + "x64 rd=%u: disp 0x%08x want 0x%08x", rd, rd32(&buf[lea + 3]), + (u32)want_disp); +} + +/* Pin x64_emit_idiom to real codegen: the rd=r8 idiom captured from + * `kit cc -target x86_64-windows -c` disassembly. A drift in the builder (and + * thus a relax tested against a fictional idiom) turns this red. */ +static void x64_golden(void) { + static const u8 want[] = {0x65, 0x4c, 0x8b, 0x04, 0x25, 0x58, 0x00, 0x00, + 0x00, 0x44, 0x8b, 0x1d, 0x00, 0x00, 0x00, 0x00, + 0x4f, 0x8b, 0x04, 0xd8, 0x4d, 0x8d, 0x80, 0x00, + 0x00, 0x00, 0x00}; + u8 buf[40]; + u32 site_off; + u32 total = x64_emit_idiom(buf, 8u /* r8 */, &site_off); + EXPECT(total == sizeof want, "x64 golden length %u != %zu", total, + sizeof want); + EXPECT(memcmp(buf, want, sizeof want) == 0, "x64 golden idiom mismatch"); +} + +/* ---- aarch64 ------------------------------------------------------------- */ + +#define AA_NOP 0xd503201fu + +static u32 aa_add_imm(u32 rd, u32 rn, u32 imm12, u32 sh) { + return 0x91000000u | (sh << 22) | ((imm12 & 0xfffu) << 10) | ((rn & 0x1fu) + << 5) | (rd & 0x1fu); +} + +/* Emit the 7-instruction Win64/aarch64 TLS idiom for `rd` (aa_tls_addr_of_win). + * Only instruction (6) — the HIGH12A add the relax keys on — needs a faithful + * encoding; the rest are placeholders the relax overwrites unconditionally. */ +static u32 aa_emit_idiom(u32* w, u32 rd) { + w[0] = 0xf9400240u | (18u << 5) | rd; /* ldr rd,[x18,#0x58] (placeholder) */ + w[1] = 0x90000010u; /* adrp x16,_tls_index */ + w[2] = aa_add_imm(16u, 16u, 0u, 0u); /* add x16,x16,:lo12: */ + w[3] = 0xb9400210u; /* ldr w16,[x16] */ + w[4] = 0xf8607a00u | rd; /* ldr rd,[rd,x16,lsl#3] */ + w[5] = aa_add_imm(rd, rd, 0u, 1u); /* add rd,rd,:secrel_hi12: (HIGH12A) */ + w[6] = aa_add_imm(rd, rd, 0u, 0u); /* add rd,rd,:secrel_lo12: (LOW12A) */ + return 5u; /* word index of the HIGH12A site the relax keys on */ +} + +/* Decode ADRP+ADD (the relax's output) back to the absolute address it + * materializes, so we confirm it equals `storage`. */ +static u64 aa_decode_adrp_add(u32 adrp, u32 add, u64 adrp_pc) { + u32 immlo = (adrp >> 29) & 0x3u; + u32 immhi = (adrp >> 5) & 0x7ffffu; + i64 imm = (i64)(((u64)immhi << 2) | immlo); + if (imm & (1ll << 20)) imm -= (1ll << 21); /* sign-extend 21 bits */ + u64 page = (adrp_pc & ~0xfffull) + ((u64)imm << 12); + return page + ((add >> 10) & 0xfffu); +} + +static void aa_check(u32 rd) { + u32 w[7]; + u32 site_i = aa_emit_idiom(w, rd); + const u64 site_pc = 0x140005000ull + site_i * 4u; + const u64 storage = 0x140009123ull; + u8* site = (u8*)&w[site_i]; + u32 i; + aa64_jit_tls_le_relax(compiler_for(KIT_ARCH_ARM_64), + R_COFF_AARCH64_SECREL_HIGH12A, site, storage, site_pc); + for (i = 0; i < 4u; ++i) /* instructions (1)-(4) -> NOP */ + EXPECT(w[i] == AA_NOP, "aa64 rd=%u: insn %u not NOP (0x%08x)", rd, i, w[i]); + EXPECT((w[4] & 0x9f000000u) == 0x90000000u && (w[4] & 0x1fu) == rd, + "aa64 rd=%u: insn(5) not ADRP rd (0x%08x)", rd, w[4]); + EXPECT((w[5] & 0xff800000u) == 0x91000000u && (w[5] & 0x1fu) == rd && + ((w[5] >> 5) & 0x1fu) == rd, + "aa64 rd=%u: insn(6) not ADD rd,rd,#imm (0x%08x)", rd, w[5]); + EXPECT(w[6] == AA_NOP, "aa64 rd=%u: insn(7) not NOP (0x%08x)", rd, w[6]); + EXPECT(aa_decode_adrp_add(w[4], w[5], site_pc - 4u) == storage, + "aa64 rd=%u: ADRP+ADD != storage", rd); +} + +int main(void) { + /* Cover the x64 length-variant branches: rd&7==4 (rsp/r12 -> lea sib), + * rd&7==5 (rbp/r13 -> mov disp8), and rd>=8 (REX.R/B). */ + static const u32 x64_rds[] = {0, 1, 4, 5, 8, 12, 13}; + static const u32 aa_rds[] = {0, 1, 9, 20}; + size_t i; + kit_unit_init(&g_u); + x64_golden(); + for (i = 0; i < sizeof x64_rds / sizeof x64_rds[0]; ++i) x64_check(x64_rds[i]); + for (i = 0; i < sizeof aa_rds / sizeof aa_rds[0]; ++i) aa_check(aa_rds[i]); + kit_unit_summary(&g_u, "jit_tls_relax_test"); + return kit_unit_status(&g_u); +}