kit

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

commit f8832785ff35d8f461594f58a894dc1d234411d0
parent 277327a487828145938a8eeca9946f9f28d76256
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue,  9 Jun 2026 15:19:59 -0700

jit: abstract the reloc/relaxation path over arch and object format

The in-process JIT linker carried two near-identical relocation loops --
kit_jit_from_image (bulk) and jit_apply_one_reloc (incremental append) -- that
each inlined arch-specific instruction byte-patching and had drifted: the
append loop held a stale, partial Mach-O TLV rewrite, and the bulk loop's TLV
relax hard-coded x16 as the descriptor-thunk scratch register and panicked on
clang objects that pick x8 (the Apple TLV ABI fixes only x0, in/out).

Unify both into one shared jit_apply_reloc() and push every arch/format
identity behind hooks and reloc-descriptor flags, so link_jit.c's reloc path
names zero RelocKind, object-format, or target.arch/obj identity:

- LinkArchDesc.jit_reloc_relax (src/arch/<arch>/reloc.c): Mach-O TLV-descriptor
  collapse (any scratch register), GOT-load collapse on the append path (via
  JitRelaxCtx.got_relaxed), and weak-undef zeroing on aarch64; PCREL_LO12
  paired-AUIPC displacement recompute on riscv (link_jit owns the LinkImage
  scan via a resolve_pair callback); NULL on x86-64.
- TLS Local-Exec classification is now flag-only: RELOC_IS_TLS_LE (ELF tp
  idiom plus the aarch64 COFF SECREL12A pair) and a new RELOC_IS_SECREL
  (x86-64 COFF SECREL, a TLS access when its target is SK_TLS). A new
  RELOC_IS_PCREL_ANCHOR marks the riscv HI20 anchors. RelocDesc.flags widened
  u8 -> u16 (the byte was full).
- The Windows _tls_index drop moves to obj_format_jit_drops_symbol_ref in
  obj_tls.c, beside obj_format_tls_model, so the format-specific pseudo-symbol
  name stays out of src/link.

Behavior-preserving: the new flags are read only by the JIT, and the frozen
reloc oracles pass unchanged (reloc_desc_test 2949, reloc_apply_test 50 -- they
check width plus the got/branch/tlvp/direct_page predicates, never the new
flags). Verified green: test-link (124), test-macho (80), test-coff (78),
test-toy (1388), jit_tls_relax_test (206), and riscv64/riscv32 jit image build.

Diffstat:
Msrc/arch/aa64/link.c | 2++
Msrc/arch/aa64/reloc.c | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/arch/riscv/link.c | 3+++
Msrc/arch/riscv/reloc.c | 21+++++++++++++++++++--
Msrc/link/link_arch.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link_jit.c | 314+++++++++++++++++++++++++++----------------------------------------------------
Msrc/link/link_reloc_desc.h | 15+++++++++++++++
Msrc/obj/obj.h | 6++++++
Msrc/obj/obj_tls.c | 14++++++++++++++
Msrc/obj/reloc.c | 3++-
Msrc/obj/reloc.h | 17++++++++++++-----
11 files changed, 300 insertions(+), 217 deletions(-)

diff --git a/src/arch/aa64/link.c b/src/arch/aa64/link.c @@ -178,6 +178,7 @@ void aa64_emit_macho_stub(u8* out, u64 stub_vaddr, u64 got_slot_vaddr) { const RelocDesc* aa64_reloc_desc(RelocKind); int aa64_reloc_apply_insn(Compiler*, RelocKind, u8*, u64, i64, u64); void aa64_jit_tls_le_relax(Compiler*, RelocKind, u8*, u64, u64); +int aa64_jit_reloc_relax(Compiler*, RelocKind, const JitRelaxCtx*); /* AArch64 __chkstk for PE/COFF: probes `x15 * 16` bytes of stack one page at a * time, then returns. Mirrors the LLVM compiler-rt implementation (chkstk.S in @@ -206,6 +207,7 @@ const LinkArchDesc link_arch_aa64 = { .reloc_desc = aa64_reloc_desc, .reloc_apply_insn = aa64_reloc_apply_insn, .jit_tls_le_relax = aa64_jit_tls_le_relax, + .jit_reloc_relax = aa64_jit_reloc_relax, .coff_chkstk_bytes = aa64_coff_chkstk, .coff_chkstk_len = sizeof aa64_coff_chkstk, diff --git a/src/arch/aa64/reloc.c b/src/arch/aa64/reloc.c @@ -15,6 +15,7 @@ #include "obj/reloc.h" #include "core/bytes.h" +#include "link/link_arch.h" static const RelocDescRow aa64_rows[] = { {R_AARCH64_JUMP26, {4, RELOC_IS_BRANCH}}, @@ -40,9 +41,10 @@ static const RelocDescRow aa64_rows[] = { {R_AARCH64_TLVP_LOAD_PAGE21, {4, RELOC_IS_TLVP}}, {R_AARCH64_TLVP_LOAD_PAGEOFF12, {4, RELOC_IS_TLVP}}, /* COFF AArch64 TLS SECREL imm12 pair: ADD-imm12 instruction relocs, - * AArch64-only, applied only into PE/COFF output. */ - {R_COFF_AARCH64_SECREL_LOW12A, {4, 0}}, - {R_COFF_AARCH64_SECREL_HIGH12A, {4, 0}}, + * AArch64-only, applied only into PE/COFF output. TLS-only, so the JIT + * classifies them as Local-Exec accesses via RELOC_IS_TLS_LE. */ + {R_COFF_AARCH64_SECREL_LOW12A, {4, RELOC_IS_TLS_LE}}, + {R_COFF_AARCH64_SECREL_HIGH12A, {4, RELOC_IS_TLS_LE}}, }; const RelocDesc* aa64_reloc_desc(RelocKind k) { @@ -281,3 +283,75 @@ void aa64_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, aa64_reloc_apply_insn(c, R_AARCH64_ADD_ABS_LO12_NC, site, storage, 0, site_pc); wr_u32_le(add_lo, 0xd503201fu); /* nop */ } + +/* In-process JIT relaxation of AArch64 indirection idioms (LinkArchDesc + * .jit_reloc_relax): the single-threaded JIT has no dynamic loader, GOT, or + * TLV resolver, so each access idiom is rewritten to address the in-image + * instance directly. Returns 1 if it owned `k`, 0 to fall through to the + * ordinary reloc apply. */ +int aa64_jit_reloc_relax(Compiler* c, RelocKind k, const JitRelaxCtx* ctx) { + u8* P_bytes = ctx->site; + + /* Weak undefined target: address-of must evaluate to NULL. An ADRP + ADD + * pair would form a PC-relative address to vaddr 0 that exceeds ±4 GiB once + * the JIT places segments far from 0 (tripping link_reloc's range check). + * Rewrite the ADRP to MOVZ rd,#0 so rd becomes 0 directly; the paired ADD's + * assembled imm12 of 0 already gives rd += 0, so leave it as add rd,rd,#0. */ + if (ctx->weak_undef_zero) { + if (k == R_AARCH64_ADR_PREL_PG_HI21 || + k == R_AARCH64_ADR_PREL_PG_HI21_NC) { + u32 rd = rd_u32_le(P_bytes) & 0x1fu; + wr_u32_le(P_bytes, 0xd2800000u | rd); /* movz rd, #0 */ + return 1; + } + if (k == R_AARCH64_ADD_ABS_LO12_NC) return 1; /* leave add rd,rd,#0 */ + } + + /* Mach-O TLV access -> ordinary in-image load. Codegen emits the 4-insn + * Apple TLV sequence: + * adrp x0, desc@TLVPPAGE (PAGE21) + * ldr x0, [x0, desc@TLVPPAGEOFF] (PAGEOFF12) <- this reloc + * ldr xN, [x0] -- load the resolver thunk from desc[0] + * blr xN -- call thunk(desc) -> &var in x0 + * With one thread the in-image .tdata/.tbss IS the single instance, and + * desc[+16] already holds the variable's in-image storage address (filled by + * the normal R_ABS64 against the storage symbol). Collapse to a direct load, + * dropping the thunk and the per-thread block: + * PAGEOFF12 : ldr x0,[x0,#imm] -> add x0,x0,#(desc & 0xfff) (x0 = &desc) + * +4 : ldr xN,[x0] -> ldr x0,[x0,#16] (x0 = &var) + * +8 : blr xN -> nop + * The thunk register N is scratch (the Apple TLV ABI fixes only x0: + * descriptor in, &var out); kit's codegen uses x16, clang picks any free + * register (e.g. x8). Accept any N so long as the pair is `ldr xN,[x0]` + * (Rn=x0, imm12=0) followed by `blr xN`. */ + if (k == R_AARCH64_TLVP_LOAD_PAGEOFF12) { + u64 v = ((u64)ctx->S + (u64)ctx->addend) & 0xfffu; + u32 instr = rd_u32_le(P_bytes); + u8* i_thunk = P_bytes + 4u; + u8* i_call = P_bytes + 8u; + u32 thunk = rd_u32_le(i_thunk); + u32 call = rd_u32_le(i_call); + u32 n = thunk & 0x1fu; + wr_u32_le(P_bytes, 0x91000000u | (instr & 0x3ffu) | ((u32)v << 10)); + if ((thunk & ~0x1fu) != 0xf9400000u || call != (0xd63f0000u | (n << 5u))) + compiler_panic(c, SRCLOC_NONE, + "kit_jit: unexpected Mach-O TLV access sequence"); + wr_u32_le(i_thunk, 0xf9400800u); /* ldr x0, [x0, #16] -> &var */ + wr_u32_le(i_call, 0xd503201fu); /* nop */ + return 1; + } + + /* No real GOT in the append image: the GOT load becomes a direct add, so the + * register holds the symbol address itself instead of loading it from a slot. + * LD64_GOT_LO12_NC shares the LDR uimm12 encoding; rewrite to ADD imm12. */ + if (ctx->got_relaxed && k == R_AARCH64_LD64_GOT_LO12_NC) { + u64 v = ((u64)ctx->S + (u64)ctx->addend) & 0xfffu; + u32 instr = rd_u32_le(P_bytes); + u32 rd = instr & 0x1fu; + u32 rn = (instr >> 5) & 0x1fu; + wr_u32_le(P_bytes, 0x91000000u | rd | (rn << 5) | ((u32)v << 10)); + return 1; + } + + return 0; +} diff --git a/src/arch/riscv/link.c b/src/arch/riscv/link.c @@ -115,6 +115,7 @@ static u32 rv32_emit_iplt_stub(u8* dst, u64 stub_vaddr, u64 slot_vaddr, const RelocDesc* rv_reloc_desc(RelocKind); int rv_reloc_apply_insn(Compiler*, RelocKind, u8*, u64, i64, u64); void rv_jit_tls_le_relax(Compiler*, RelocKind, u8*, u64, u64); +int rv_jit_reloc_relax(Compiler*, RelocKind, const JitRelaxCtx*); const LinkArchDesc link_arch_rv64 = { .plt0_size = RV64_PLT0_SIZE, @@ -128,6 +129,7 @@ const LinkArchDesc link_arch_rv64 = { .reloc_desc = rv_reloc_desc, .reloc_apply_insn = rv_reloc_apply_insn, .jit_tls_le_relax = rv_jit_tls_le_relax, + .jit_reloc_relax = rv_jit_reloc_relax, }; /* RV32 link descriptor: identical to rv64 (PLT0/entry/stub byte sizes, @@ -145,4 +147,5 @@ const LinkArchDesc link_arch_rv32 = { .emit_iplt_stub = rv32_emit_iplt_stub, .reloc_desc = rv_reloc_desc, .reloc_apply_insn = rv_reloc_apply_insn, + .jit_reloc_relax = rv_jit_reloc_relax, }; diff --git a/src/arch/riscv/reloc.c b/src/arch/riscv/reloc.c @@ -20,6 +20,7 @@ #include "obj/reloc.h" #include "core/bytes.h" +#include "link/link_arch.h" static const RelocDescRow rv_rows[] = { {R_RV_HI20, {4, 0}}, @@ -27,10 +28,10 @@ static const RelocDescRow rv_rows[] = { {R_RV_LO12_S, {4, 0}}, {R_RV_BRANCH, {4, 0}}, {R_RV_JAL, {4, 0}}, - {R_RV_PCREL_HI20, {4, 0}}, + {R_RV_PCREL_HI20, {4, RELOC_IS_PCREL_ANCHOR}}, {R_RV_PCREL_LO12_I, {4, 0}}, {R_RV_PCREL_LO12_S, {4, 0}}, - {R_RV_GOT_HI20, {4, RELOC_USES_GOT}}, + {R_RV_GOT_HI20, {4, RELOC_USES_GOT | RELOC_IS_PCREL_ANCHOR}}, {R_RV_TLS_GOT_HI20, {4, RELOC_IS_TLS_GOT}}, {R_RV_TPREL_HI20, {4, RELOC_IS_TLS_LE}}, {R_RV_TPREL_LO12_I, {4, RELOC_IS_TLS_LE}}, @@ -302,3 +303,19 @@ void rv_jit_tls_le_relax(Compiler* c, RelocKind k, u8* site, u64 storage, addi_w = rd_u32_le(addi); wr_u32_le(addi, (addi_w & 0x000fffffu) | (((u32)lo12 & 0xfffu) << 20)); } + +/* In-process JIT relaxation of RISC-V indirection idioms (LinkArchDesc + * .jit_reloc_relax). Today only PCREL_LO12: it has no S of its own — its + * low-12 bits must match the paired AUIPC's PC-relative displacement, which + * the JIT recomputes against its layout via ctx->resolve_pair. Feed that + * displacement to the LO12_I/S encoder (same instruction encoding; the addend + * is unused per the psABI). Returns 1 if it owned `k`, 0 otherwise. */ +int rv_jit_reloc_relax(Compiler* c, RelocKind k, const JitRelaxCtx* ctx) { + if (k == R_RV_PCREL_LO12_I || k == R_RV_PCREL_LO12_S) { + i64 disp = ctx->resolve_pair(ctx->resolve_user, ctx->anchor_vaddr); + RelocKind alias = (k == R_RV_PCREL_LO12_I) ? R_RV_LO12_I : R_RV_LO12_S; + rv_reloc_apply_insn(c, alias, ctx->site, (u64)disp, 0, ctx->site_pc); + return 1; + } + return 0; +} diff --git a/src/link/link_arch.h b/src/link/link_arch.h @@ -32,6 +32,38 @@ typedef struct LinkArchIPltReloc { RelocKind kind; } LinkArchIPltReloc; +/* Context for LinkArchDesc.jit_reloc_relax — the in-process JIT's per-reloc + * "collapse runtime indirection to direct in-image addressing" pass. The + * single-threaded JIT has no dynamic loader, GOT, or TLV resolver, so the + * arch rewrites the access idiom in place. The hook reads only the fields + * relevant to the kinds it owns; the rest cost nothing. */ +typedef struct JitRelaxCtx { + u8* site; /* write-alias bytes at the reloc site */ + u64 S; /* resolved runtime symbol address (0 when weak_undef_zero) */ + i64 addend; /* the reloc's addend */ + u64 site_pc; /* runtime address of the reloc site */ + + /* Address-of a weak *undefined* symbol must read as 0. Set when the target + * is weak + SK_ABS + vaddr==0; the arch zeroes the materialized register + * rather than forming a (huge, out-of-range) PC-relative address. */ + int weak_undef_zero; + + /* The image has no real GOT — the incremental-append path does not re-run + * GOT layout, so GOT-load relocs must be relaxed to direct in-image + * addressing. 0 for the full from-image link, where .got slots exist and + * GOT loads apply normally (S already points at the slot). */ + int got_relaxed; + + /* RISC-V PCREL_LO12 pairs with an AUIPC anchor whose runtime PC-relative + * displacement must be recomputed against the JIT layout. resolve_pair + * returns that displacement for the anchor at image vaddr `anchor_vaddr`; + * the arch calls it opaquely (the implementation, in link_jit.c, needs + * LinkImage internals the arch must not reach). */ + i64 (*resolve_pair)(void* user, u64 anchor_vaddr); + void* resolve_user; + u64 anchor_vaddr; +} JitRelaxCtx; + typedef struct LinkArchDesc { /* ---- PLT geometry ---- * All three arches today use a 32-byte PLT0 + 16-byte per-import @@ -112,6 +144,16 @@ typedef struct LinkArchDesc { void (*jit_tls_le_relax)(Compiler* c, RelocKind k, u8* site, u64 storage, u64 site_pc); + /* In-process JIT only: collapse one relocation's runtime indirection to + * direct in-image addressing — the Mach-O TLV descriptor call, a GOT load + * (append path), the weak-undef-zero materialization, and the RISC-V + * PCREL_LO12 anchor recompute. The arch owns the instruction byte rewrites + * for exactly the kinds it recognizes (consulting JitRelaxCtx for the + * weak/GOT/pair context). Returns 1 if it handled `k`, 0 to fall through to + * link_reloc_apply. NULL on arches with no JIT relaxation beyond TLS LE + * (x86-64). */ + int (*jit_reloc_relax)(Compiler* c, RelocKind k, const JitRelaxCtx* ctx); + /* TLS variant: 1 = variant II (x86-64, tpoff = X - tls_memsz_rounded); * 0 = variant I (AArch64/RISC-V, tpoff = (X - tls_vaddr) + tcb_bias). * Consulted by the ELF linker when applying R_TPOFF64. */ diff --git a/src/link/link_jit.c b/src/link/link_jit.c @@ -121,38 +121,24 @@ 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. */ +/* A reference the single-threaded JIT drops because the format's TLS access + * idiom that materializes its target is relaxed in-image. On Windows/COFF the + * idiom reads TEB.ThreadLocalStoragePointer, indexes it by the module's + * `_tls_index`, then `+ SECREL(var)` — there is no loaded PE for the OS to + * assign a `_tls_index`, so jit_tls_le_relax rewrites that load away and the + * `_tls_index` ADRP/PC32 relocs must not be applied. The format authority + * (obj_format_jit_drops_symbol_ref) owns which symbol that is. */ 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); + return tgt && obj_format_jit_drops_symbol_ref(c, tgt->name); } -/* 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. */ +/* TLS Local-Exec classifier for the JIT reloc passes, entirely flag-driven: + * the per-arch tp-relative idiom and the aa64 COFF SECREL12A pair carry + * RELOC_IS_TLS_LE; x86-64's generic COFF SECREL (RELOC_IS_SECREL) is a TLS + * access exactly when its target is a TLS symbol. */ 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); + return reloc_kind_is_tls_le(c, k) || + (reloc_kind_is_secrel(c, k) && tgt_kind == SK_TLS); } /* Find the segment that contains image-relative `vaddr` and return its @@ -217,7 +203,7 @@ static i64 jit_rv_pcrel_lo12_disp(LinkImage* img, KitExecMemRegion* segs, const LinkRelocApply* hi = LinkRelocs_at(&img->relocs, i); const LinkSymbol* hi_tgt; u64 hi_S, hi_P; - if (hi->kind != R_RV_PCREL_HI20 && hi->kind != R_RV_GOT_HI20) continue; + if (!reloc_kind_is_pcrel_anchor(img->c, hi->kind)) continue; if (hi->write_vaddr != auipc_image_vaddr) continue; hi_tgt = LinkSyms_at(&img->syms, hi->target - 1); if (!hi_tgt) continue; @@ -267,6 +253,92 @@ static void jit_copy_input_section_bytes(LinkImage* img, } } +/* Opaque user data for the JitRelaxCtx.resolve_pair callback: the arch hook + * (RISC-V PCREL_LO12) gets the paired AUIPC's runtime displacement without + * reaching into LinkImage itself. */ +typedef struct { + LinkImage* img; + KitExecMemRegion* segs; +} JitPairResolve; + +static i64 jit_resolve_pcrel_pair(void* user, u64 anchor_vaddr) { + JitPairResolve* p = (JitPairResolve*)user; + return jit_rv_pcrel_lo12_disp(p->img, p->segs, anchor_vaddr); +} + +/* Apply one relocation against the JIT runtime base. Shared by the bulk + * from-image pass and the incremental-append pass so their reloc handling can + * never drift. Patch-site bytes go through the write alias; PC-relative + * arithmetic uses the runtime alias. + * + * Order mirrors the resolution chain: drop Windows `_tls_index` refs, relax + * Local-Exec TLS via the arch idiom hook, then resolve S and offer the reloc + * to the arch's JIT-relaxation hook (TLV / GOT / weak-undef / RISC-V pcrel + * pairing) before the ordinary apply. `got_relaxed` is 1 on the append path + * (no real GOT was built) and 0 for the full link (where .got slots exist). */ +static void jit_apply_reloc(Compiler* c, LinkImage* img, KitExecMemRegion* segs, + const LinkRelocApply* r, int got_relaxed) { + const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1); + const LinkArchDesc* d; + JitPairResolve pair; + JitRelaxCtx ctx; + u64 S, P; + u8* P_bytes; + + /* 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)) return; + + /* Local-Exec TLS -> in-image addressing (single-threaded JIT). The per-arch + * idiom rewrite lives behind LinkArchDesc.jit_tls_le_relax; this 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)) { + u64 storage = (tgt->kind == SK_ABS) + ? tgt->vaddr + (u64)r->addend + : (u64)vaddr_to_runtime(img, segs, tgt->vaddr) + + (u64)r->addend; + u8* bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); + u64 site_pc = (u64)vaddr_to_runtime(img, segs, r->write_vaddr); + d = link_arch_desc_for(c); + if (!d || !d->jit_tls_le_relax || !bytes) + compiler_panic(c, SRCLOC_NONE, + "kit_jit: target has no TLS Local-Exec relaxation"); + d->jit_tls_le_relax(c, r->kind, bytes, storage, site_pc); + return; + } + + /* extern resolver result OR true absolute symbol — vaddr already holds the + * runtime address; otherwise map the image vaddr to its runtime alias. */ + S = (tgt->kind == SK_ABS) ? tgt->vaddr + : (u64)vaddr_to_runtime(img, segs, tgt->vaddr); + P = (u64)vaddr_to_runtime(img, segs, r->write_vaddr); + P_bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); + if (!P_bytes) + compiler_panic(c, SRCLOC_NONE, "kit_jit: relocation site is unmapped"); + + /* Arch JIT relaxation: collapse the runtime indirection (Mach-O TLV + * descriptor call, GOT load on the append path, weak-undef zeroing, RISC-V + * PCREL_LO12 anchor recompute) to direct in-image addressing. Returns 1 if + * it owned the kind; otherwise fall through to the ordinary apply. */ + pair.img = img; + pair.segs = segs; + ctx.site = P_bytes; + ctx.S = S; + ctx.addend = r->addend; + ctx.site_pc = P; + ctx.weak_undef_zero = + (tgt->bind == SB_WEAK && tgt->kind == SK_ABS && tgt->vaddr == 0); + ctx.got_relaxed = got_relaxed; + ctx.resolve_pair = jit_resolve_pcrel_pair; + ctx.resolve_user = &pair; + ctx.anchor_vaddr = tgt->vaddr; + d = link_arch_desc_for(c); + if (d && d->jit_reloc_relax && d->jit_reloc_relax(c, r->kind, &ctx)) return; + + link_reloc_apply(c, r->kind, P_bytes, S, r->addend, P); +} + KitJit* kit_jit_from_image(LinkImage* img) { Compiler* c; Heap* heap; @@ -389,111 +461,9 @@ KitJit* kit_jit_from_image(LinkImage* img) { * alias; PC-relative arithmetic uses the runtime alias address. */ metrics_scope_begin(c, "jit.apply_relocs"); for (i = 0; i < LinkRelocs_count(&img->relocs); ++i) { - const LinkRelocApply* r = LinkRelocs_at(&img->relocs, i); - const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1); - u64 S, P; - u8* P_bytes; - /* 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 - : (u64)vaddr_to_runtime(img, segs, tgt->vaddr) + - (u64)r->addend; - u8* bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); - u64 site_pc = (u64)vaddr_to_runtime(img, segs, r->write_vaddr); - if (!d || !d->jit_tls_le_relax || !bytes) - compiler_panic(c, SRCLOC_NONE, - "kit_jit: target has no TLS Local-Exec relaxation"); - d->jit_tls_le_relax(c, r->kind, bytes, storage, site_pc); - continue; - } - if (r->kind == R_RV_PCREL_LO12_I || r->kind == R_RV_PCREL_LO12_S) { - /* RISC-V PCREL_LO12: target.vaddr is the paired AUIPC site - * (a local anchor symbol). Recompute the AUIPC's runtime - * displacement and feed it as S to the LO12_I/S apply path so - * the encoded low-12 bits match the AUIPC's HI20. The reloc's - * own addend is unused per the psABI. */ - i64 disp = jit_rv_pcrel_lo12_disp(img, segs, tgt->vaddr); - RelocKind alias = - (r->kind == R_RV_PCREL_LO12_I) ? R_RV_LO12_I : R_RV_LO12_S; - P_bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); - link_reloc_apply(c, alias, P_bytes, (u64)disp, 0, - (u64)vaddr_to_runtime(img, segs, r->write_vaddr)); - continue; - } else if (tgt->kind == SK_ABS) { - /* extern resolver result OR true absolute symbol — vaddr - * already holds the runtime address. */ - S = tgt->vaddr; - } else { - S = (u64)vaddr_to_runtime(img, segs, tgt->vaddr); - } - P = (u64)vaddr_to_runtime(img, segs, r->write_vaddr); - P_bytes = (u8*)vaddr_to_write(img, segs, r->write_vaddr); - /* Weak-undef target: vaddr is 0, address-of must evaluate to NULL - * (§"weak attribute resolves to 0 at link time"). For an AArch64 - * ADRP + ADD pair against such a target, the PCREL displacement - * exceeds ±4 GiB once the JIT places segments far from address 0, - * which would trip link_reloc's range check. Rewrite the ADRP to - * MOVZ Xd, #0 so Xd becomes 0 directly; the paired ADD's imm12 - * default of 0 already gives Xd += 0, so the LO12_NC reloc is a - * no-op and we skip it. Dereferencing the resulting NULL is UB, - * same as GCC/Clang's behavior for weak loads. */ - if (tgt->bind == SB_WEAK && tgt->kind == SK_ABS && tgt->vaddr == 0) { - if (r->kind == R_AARCH64_ADR_PREL_PG_HI21 || - r->kind == R_AARCH64_ADR_PREL_PG_HI21_NC) { - u32 instr = rd_u32_le(P_bytes); - u32 rd = instr & 0x1fu; - wr_u32_le(P_bytes, 0xd2800000u | rd); /* movz Xrd, #0 */ - continue; - } - if (r->kind == R_AARCH64_ADD_ABS_LO12_NC) { - /* The default imm12 in the assembled ADD is 0 (the assembler - * placeholder), so leaving the site unpatched encodes ADD Xd, - * Xd, #0 — exactly what we want after the ADRP→MOVZ rewrite. */ - continue; - } - } - /* Mach-O TLV access -> ordinary in-image load (single-threaded JIT). - * - * Codegen emits the Apple TLV sequence (4 insns): - * adrp x0, desc@TLVPPAGE (PAGE21) - * ldr x0, [x0, desc@TLVPPAGEOFF] (PAGEOFF12) -- via a __thread_ptrs slot - * ldr x16, [x0] -- load the resolver thunk from desc[+0] - * blr x16 -- call thunk(desc) -> &var in x0 - * The AOT writer/dyld back that with a thunk + per-thread block. With one - * thread the in-image .tdata/.tbss IS the single instance, and desc[+16] - * already holds the variable's in-image storage address (the normal - * R_ABS64 against the storage symbol the loop applies). So we collapse the - * access to a direct load, dropping the thunk, the per-thread block, and - * descriptor patching entirely: - * PAGE21 : adrp x0, desc (unchanged, applied below) - * PAGEOFF12 : ldr x0,[x0,#imm] -> add x0,x0,#(desc & 0xfff) (x0 = &desc) - * +4 : ldr x16,[x0] -> ldr x0,[x0,#16] (x0 = &var) - * +8 : blr x16 -> nop - * (LDR uimm12 64-bit bits[31:22]=0x3E5; ADD imm sh=0 bits[31:22]=0x244; - * Rn[9:5]/Rt[4:0]/imm12[21:10] keep their positions.) */ - if (r->kind == R_AARCH64_TLVP_LOAD_PAGEOFF12) { - u64 v = ((u64)S + (u64)r->addend) & 0xfffu; - u32 instr = rd_u32_le(P_bytes); - u8* i_thunk = P_bytes + 4u; /* ldr x16, [x0] */ - u8* i_call = P_bytes + 8u; /* blr x16 */ - instr = 0x91000000u | (instr & 0x3ffu) | ((u32)v << 10); - wr_u32_le(P_bytes, instr); - if (rd_u32_le(i_thunk) != 0xf9400010u || rd_u32_le(i_call) != 0xd63f0200u) - compiler_panic(c, SRCLOC_NONE, - "kit_jit: unexpected Mach-O TLV access sequence"); - wr_u32_le(i_thunk, 0xf9400800u); /* ldr x0, [x0, #16] -> &var */ - wr_u32_le(i_call, 0xd503201fu); /* nop */ - continue; - } - link_reloc_apply(c, r->kind, P_bytes, S, r->addend, P); + /* got_relaxed=0: the full from-image link ran GOT layout, so .got slots + * exist and GOT loads apply normally (S points at the slot). */ + jit_apply_reloc(c, img, segs, LinkRelocs_at(&img->relocs, i), 0); } metrics_scope_end(c, "jit.apply_relocs"); @@ -734,79 +704,11 @@ static void jit_invalidate_view(KitJit* jit) { jit->view_built = 0u; } +/* Incremental-append reloc apply. Delegates to the shared jit_apply_reloc + * with got_relaxed=1: the append path does not re-run GOT layout, so GOT loads + * in the appended object are relaxed to direct in-image addressing. */ static void jit_apply_one_reloc(KitJit* jit, const LinkRelocApply* r) { - LinkImage* img = jit->image; - const LinkSymbol* tgt = LinkSyms_at(&img->syms, r->target - 1); - u64 S; - u64 P; - u8* P_bytes; - /* 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 - : (u64)vaddr_to_runtime(img, jit->segs, tgt->vaddr) + - (u64)r->addend; - u8* bytes = (u8*)vaddr_to_write(img, jit->segs, r->write_vaddr); - u64 site_pc = (u64)vaddr_to_runtime(img, jit->segs, r->write_vaddr); - if (!d || !d->jit_tls_le_relax || !bytes) - compiler_panic(jit->c, SRCLOC_NONE, - "kit_jit_append_obj: no TLS Local-Exec relaxation"); - d->jit_tls_le_relax(jit->c, r->kind, bytes, storage, site_pc); - return; - } - if (r->kind == R_RV_PCREL_LO12_I || r->kind == R_RV_PCREL_LO12_S) { - i64 disp = jit_rv_pcrel_lo12_disp(img, jit->segs, tgt->vaddr); - RelocKind alias = - (r->kind == R_RV_PCREL_LO12_I) ? R_RV_LO12_I : R_RV_LO12_S; - P_bytes = (u8*)vaddr_to_write(img, jit->segs, r->write_vaddr); - if (!P_bytes) - compiler_panic(jit->c, SRCLOC_NONE, - "kit_jit_append_obj: relocation site is unmapped"); - link_reloc_apply(jit->c, alias, P_bytes, (u64)disp, 0, - (u64)vaddr_to_runtime(img, jit->segs, r->write_vaddr)); - return; - } - if (tgt->kind == SK_ABS) { - S = tgt->vaddr; - } else { - S = (u64)vaddr_to_runtime(img, jit->segs, tgt->vaddr); - } - P = (u64)vaddr_to_runtime(img, jit->segs, r->write_vaddr); - P_bytes = (u8*)vaddr_to_write(img, jit->segs, r->write_vaddr); - if (!P_bytes) - compiler_panic(jit->c, SRCLOC_NONE, - "kit_jit_append_obj: relocation site is unmapped"); - if (tgt->bind == SB_WEAK && tgt->kind == SK_ABS && tgt->vaddr == 0) { - if (r->kind == R_AARCH64_ADR_PREL_PG_HI21 || - r->kind == R_AARCH64_ADR_PREL_PG_HI21_NC) { - u32 instr = rd_u32_le(P_bytes); - u32 rd = instr & 0x1fu; - wr_u32_le(P_bytes, 0xd2800000u | rd); - return; - } - if (r->kind == R_AARCH64_ADD_ABS_LO12_NC) return; - } - if (r->kind == R_AARCH64_TLVP_LOAD_PAGEOFF12) { - u64 v = ((u64)S + (u64)r->addend) & 0xfffu; - u32 instr = rd_u32_le(P_bytes); - instr = 0x91000000u | (instr & 0x3ffu) | ((u32)v << 10); - wr_u32_le(P_bytes, instr); - return; - } - if (r->kind == R_AARCH64_LD64_GOT_LO12_NC) { - u64 v = ((u64)S + (u64)r->addend) & 0xfffu; - u32 instr = rd_u32_le(P_bytes); - u32 rd = instr & 0x1fu; - u32 rn = (instr >> 5) & 0x1fu; - wr_u32_le(P_bytes, 0x91000000u | rd | (rn << 5) | ((u32)v << 10)); - return; - } - link_reloc_apply(jit->c, r->kind, P_bytes, S, r->addend, P); + jit_apply_reloc(jit->c, jit->image, jit->segs, r, 1); } static void jit_append_obj_inner(KitJit* jit, ObjBuilder* ob) { diff --git a/src/link/link_reloc_desc.h b/src/link/link_reloc_desc.h @@ -72,4 +72,19 @@ static inline int reloc_kind_is_direct_page(const Compiler* c, RelocKind k) { return d && (d->flags & RELOC_DIRECT_PAGE) ? 1 : 0; } +/* PC-relative HI20 anchor (RISC-V AUIPC for PCREL_HI20 / GOT_HI20) that a + * paired PCREL_LO12 resolves against. The in-process JIT scans for the anchor + * to recompute the low-12 displacement under its layout. */ +static inline int reloc_kind_is_pcrel_anchor(const Compiler* c, RelocKind k) { + const RelocDesc* d = reloc_desc(c, k); + return d && (d->flags & RELOC_IS_PCREL_ANCHOR) ? 1 : 0; +} + +/* COFF section-relative value (R_COFF_SECREL). A TLS Local-Exec access exactly + * when its target is a TLS symbol; the in-process JIT relaxes those. */ +static inline int reloc_kind_is_secrel(const Compiler* c, RelocKind k) { + const RelocDesc* d = reloc_desc(c, k); + return d && (d->flags & RELOC_IS_SECREL) ? 1 : 0; +} + #endif diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -815,6 +815,12 @@ typedef enum ObjTlsModel { * wrapper over (model == OBJ_TLS_MACHO_DESCRIPTOR). */ ObjTlsModel obj_format_tls_model(const Compiler*); +/* In-process JIT: true when a reference to symbol `name` is dropped because the + * format's TLS access idiom that materializes it is relaxed to in-image + * addressing (COFF Windows `_tls_index`; none elsewhere). Beside + * obj_format_tls_model as the TLS-mechanism authority. */ +int obj_format_jit_drops_symbol_ref(const Compiler*, Sym name); + /* True when the active object format carries DWARF debug sections * file-only (not mapped into a loadable segment): ELF / Mach-O yes, * COFF no. */ diff --git a/src/obj/obj_tls.c b/src/obj/obj_tls.c @@ -61,6 +61,20 @@ int obj_format_tls_via_descriptor(const Compiler* c) { return obj_format_tls_model(c) == OBJ_TLS_MACHO_DESCRIPTOR; } +/* In-process JIT: 1 when a reference to symbol `name` is dropped because the + * access idiom that materializes it is relaxed to in-image addressing. The + * COFF Windows TEB model loads a per-module `_tls_index`; the JIT rewrites that + * load away, so its relocs must not be applied. None elsewhere. Sits beside + * obj_format_tls_model as the TLS-mechanism authority, so src/link stays free + * of the format-specific pseudo-symbol name. */ +int obj_format_jit_drops_symbol_ref(const Compiler* c, Sym name) { + Slice nm; + if (!c || name == 0) return 0; + if (obj_format_tls_model(c) != OBJ_TLS_WINDOWS_TEB) return 0; + nm = pool_slice(c->global, name); + return nm.len == 10u && memcmp(nm.s, "_tls_index", 10u) == 0; +} + static void define_tls_elf(ObjBuilder* ob, Compiler* c, ObjSymId sym, const u8* data, u32 size, int has_nonzero_init, u32 align, const ObjTlsReloc* relocs, u32 nrelocs) { diff --git a/src/obj/reloc.c b/src/obj/reloc.c @@ -35,7 +35,8 @@ static const RelocDescRow neutral_rows[] = { {R_REL32, {4, 0}}, {R_REL64, {8, 0}}, {R_PC32, {4, 0}}, {R_PC64, {8, 0}}, {R_GOT32, {4, 0}}, {R_PLT32, {4, 0}}, - {R_COFF_SECREL, {4, 0}}, {R_COFF_SECTION, {2, 0}}, + {R_COFF_SECREL, {4, RELOC_IS_SECREL}}, + {R_COFF_SECTION, {2, 0}}, {R_COFF_ADDR32NB, {4, 0}}, }; diff --git a/src/obj/reloc.h b/src/obj/reloc.h @@ -23,14 +23,21 @@ typedef enum RelocDescFlag { RELOC_DIRECT_PAGE = 1u << 4, /* Mach-O ADRP-direct (non-GOT) page / pageoff */ RELOC_MARKER = 1u << 5, /* no bytes patched (RELAX / TPREL_ADD) */ RELOC_WIDTH_DYN = 1u << 6, /* width read from the bytes at apply (ULEB128) */ - RELOC_IS_TLS_LE = 1u << 7, /* ELF Local-Exec TLS access (tp-relative idiom); - * the in-process JIT relaxes it to in-image - * addressing via LinkArchDesc.jit_tls_le_relax */ + RELOC_IS_TLS_LE = 1u << 7, /* ELF Local-Exec TLS access (tp-relative idiom), + * plus the COFF Windows SECREL12A TLS pair; the + * in-process JIT relaxes it to in-image + * addressing via LinkArchDesc.jit_tls_le_relax */ + RELOC_IS_PCREL_ANCHOR = 1u << 8, /* PC-relative HI20 anchor (RISC-V AUIPC) a + * paired PCREL_LO12 reloc resolves against; + * the JIT recomputes its displacement */ + RELOC_IS_SECREL = 1u << 9, /* COFF section-relative value (R_COFF_SECREL); a + * TLS Local-Exec access when its target is a TLS + * symbol, which the in-process JIT relaxes */ } RelocDescFlag; typedef struct RelocDesc { - u8 width; /* patched-field width in bytes; nominal when RELOC_WIDTH_DYN */ - u8 flags; /* RelocDescFlag bitset */ + u8 width; /* patched-field width in bytes; nominal when RELOC_WIDTH_DYN */ + u16 flags; /* RelocDescFlag bitset */ } RelocDesc; /* One row of a per-arch / neutral descriptor table. `kind` holds a