kit

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

commit 0020f1ce0b5092ec7547fea6464babcb19b42836
parent 7336010e1c2df57fdc23b794f497d361843c796a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 17:06:21 -0700

fix(obj): make COFF/Mach-O objects linkable by system linkers

kit-emitted objects were rejected by external linkers (LLVM lld's
ELF/COFF/Mach-O personalities and Apple ld); only kit's own `ld` consumed
them. Two bugs:

- COFF: NOBITS/.bss sections wrote their size into VirtualSize with
  SizeOfRawData=0. COFF object files carry the size in SizeOfRawData
  (VirtualSize is image-only); lld treated SizeOfRawData==0 as empty,
  discarded .bss, and every relocation to a global defined there dangled
  ("relocation against symbol in discarded section"). Now: size in
  SizeOfRawData, VirtualSize 0, no raw data (matches clang/MSVC). The COFF
  reader already falls back to SizeOfRawData, so round-trip is stable.

- Mach-O: the DWARF FDE pc-begin was emitted as R_PC32, which on arm64
  Mach-O has no representation and became a pcrel UNSIGNED reloc ld64 /
  ld64.lld reject ("UNSIGNED relocation must not be PC-relative"; Apple ld:
  "empty CIE"). clang uses __compact_unwind instead. kit now stops emitting
  __eh_frame on Mach-O (the emits_eh_frame target gate excludes
  KIT_OBJ_MACHO); macOS backtraces walk the frame-pointer chain, so no
  capability is lost. ELF/COFF keep .eh_frame.

ELF (aa64/x64/rv64) already linked with ld.lld. Regression guard:
test/extlink/run.sh / `make test-extlink` links the corpus with ld.lld,
lld-link, ld64.lld, and Apple ld (link + native run); lanes self-skip when
a linker/SDK is absent. Dropping the Mach-O __eh_frame makes --strip-all
correctly drop _main (the FDE reloc used to keep it alive), matching the
strip tests' documented intent — strip/objcopy goldens updated.

Known follow-up: x86_64-macos still mis-emits __text addend relocs (the
arm64-only ARM64_RELOC_ADDEND pair path runs for x64); see doc/OBJ.md.

Diffstat:
Mdoc/OBJ.md | 14++++++++++++++
Minclude/kit/core.h | 3++-
Mmk/test.mk | 9+++++++++
Msrc/api/core.c | 13+++++++++++--
Msrc/obj/coff/emit.c | 16++++++++++++----
Atest/extlink/run.sh | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/objcopy/cases/01-rename-section.expected | 1-
Mtest/objcopy/cases/04-add-section.expected | 1-
Mtest/strip/cases/01-strip-debug.expected | 1-
Mtest/strip/cases/02-strip-all-keeps-reloc-targets.expected | 2--
Mtest/strip/cases/03-keep-symbol.expected | 1-
Mtest/strip/cases/04-archive-strip-debug.expected | 2--
12 files changed, 183 insertions(+), 15 deletions(-)

diff --git a/doc/OBJ.md b/doc/OBJ.md @@ -312,6 +312,20 @@ linker's `-l` path: `read_macho_dso` (MH_DYLIB exports) and `tbd_read.c` (Apple model, the `__DWARF` segment section-name spellings — are concentrated in `obj_secnames.c`/`obj_tls.c` and the writer, not the backends. +Mach-O objects deliberately carry **no `__eh_frame`** (the `emits_eh_frame` +target gate in `src/api/core.c` excludes `KIT_OBJ_MACHO`). arm64 Mach-O has no +pcrel-32 data relocation for a DWARF FDE pc-begin, and ld64's legacy +`__eh_frame` path is effectively unused upstream — clang emits +`__compact_unwind` instead — so kit's raw `__eh_frame` made objects unlinkable +by `ld64`/`ld64.lld` ("UNSIGNED relocation must not be PC-relative", "empty +CIE"). kit's macOS backtraces walk the frame-pointer chain, so dropping it +costs no capability. `test/extlink/run.sh` (the `test-extlink` target) is the +regression guard: it links kit objects with the system linkers across all three +formats. Known follow-up: `x86_64-macos` still mis-emits the `__text` +data-access addend reloc (the arm64-only `ARM64_RELOC_ADDEND` pair path in +`macho/emit.c` runs for x86_64, where the addend must instead fold inline into a +single `X86_64_RELOC_SIGNED`); the arm64 path is correct. + ### COFF / PE (Windows, `src/obj/coff/`) 64-bit only (`x86_64-windows`, `aarch64-windows`); the hosted profile is diff --git a/include/kit/core.h b/include/kit/core.h @@ -206,7 +206,8 @@ typedef struct KitTargetSpec { uint8_t long_size; /* sizeof(long): 8 for LP64, else 4 (LLP64/ILP32) */ uint8_t wchar_size; /* sizeof(wchar_t): 2 on Windows, else 4 */ uint8_t long_double_format; /* KitLongDoubleFormat */ - uint8_t emits_eh_frame; /* 1 when os != KIT_OS_FREESTANDING, else 0 */ + uint8_t emits_eh_frame; /* 1 for hosted ELF/COFF; 0 for freestanding + and Mach-O (ld64 rejects raw __eh_frame) */ uint8_t os_version_major; /* OS major version (FreeBSD: 14/15/…); 0 = unspecified */ } KitTargetSpec; diff --git a/mk/test.mk b/mk/test.mk @@ -80,6 +80,7 @@ TEST_TARGETS = \ test-driver-strip \ test-dwarf \ test-elf \ + test-extlink \ test-emu \ test-emu-unit \ test-interp \ @@ -172,6 +173,7 @@ DEFAULT_TEST_TARGETS = \ test-dbg \ test-disasm-complete \ test-macho \ + test-extlink \ test-interp-toy \ test-wasm \ test-libc \ @@ -299,6 +301,13 @@ test-driver-objdump: bin test-driver-pkg: bin @KIT=$(abspath $(BIN)) sh test/pkg/run.sh +# test-extlink: prove kit-emitted objects link with the *system* linkers +# (LLVM lld ELF/COFF/Mach-O personalities + Apple ld), not just kit's own +# `ld`. Lanes self-skip when a linker / SDK is absent, so it is safe in the +# default set on hosts without lld installed. +test-extlink: bin + @KIT=$(abspath $(BIN)) sh test/extlink/run.sh + # test-dist: one-command verification of the dist subsystem (content-addressed # store + signed packaging). Runs the CAS and pkg driver corpora, which cover # round-trip, integrity/tamper detection, trust store, and the read-side diff --git a/src/api/core.c b/src/api/core.c @@ -72,8 +72,17 @@ KitStatus kit_target_new(const KitContext* ctx, const KitTargetOptions* opts, } else { t->spec.long_double_format = (uint8_t)KIT_LDBL_DOUBLE; } - /* #17 eh_frame: hosted targets emit .eh_frame; freestanding does not. */ - t->spec.emits_eh_frame = (t->spec.os != KIT_OS_FREESTANDING) ? 1u : 0u; + /* #17 eh_frame: hosted ELF/COFF targets emit .eh_frame for the host + * unwinder; freestanding does not (no unwinder, and it would orphan an + * ALLOC section). Mach-O is also excluded: ld64 / ld64.lld reject kit's + * raw __eh_frame — arm64 Mach-O has no pcrel-32 data relocation for the + * FDE pc-begin, and ld64's legacy __eh_frame path is effectively unused + * (clang emits __compact_unwind instead). kit's macOS backtrace walks + * the frame-pointer chain, so dropping __eh_frame costs no capability + * while making kit objects linkable by the system linker. */ + t->spec.emits_eh_frame = + (t->spec.os != KIT_OS_FREESTANDING && t->spec.obj != KIT_OBJ_MACHO) ? 1u + : 0u; nwords = (arch->ntarget_features + 63u) / 64u; if (nwords) { diff --git a/src/obj/coff/emit.c b/src/obj/coff/emit.c @@ -66,8 +66,8 @@ static int coff_rel32_absorbs_minus4(KitArchKind arch, RelocKind kind, typedef struct CSec { /* IMAGE_SECTION_HEADER fields (little-endian-encoded at write time). */ char name8[8]; /* Name field bytes; "/N" form if long name */ - u32 virtual_size; /* nonzero for NOBITS (bss size) */ - u32 size_of_raw_data; /* zero for NOBITS */ + u32 virtual_size; /* 0 for .obj (VirtualSize is image-only) */ + u32 size_of_raw_data; /* section size, incl. NOBITS (bss) size */ u32 pointer_to_raw_data; u32 pointer_to_relocations; u16 number_of_relocations; @@ -317,8 +317,16 @@ void emit_coff(Compiler* c, ObjBuilder* ob, Writer* w) { if (s->sem == SSEM_NOBITS || s->kind == SEC_BSS) { cs->is_nobits = 1; - cs->virtual_size = s->bss_size; - cs->size_of_raw_data = 0; + /* COFF object files carry the section size in SizeOfRawData (with + * PointerToRawData=0 / no file bytes for uninitialized data) and + * keep VirtualSize=0 — VirtualSize is meaningful only for linked + * images. lld / link.exe treat a section with SizeOfRawData==0 as + * empty and discard it, orphaning every symbol defined there and + * breaking relocations that target them ("relocation against + * symbol in discarded section"). Match clang/MSVC: size in + * SizeOfRawData, VirtualSize 0, no raw data. */ + cs->virtual_size = 0; + cs->size_of_raw_data = s->bss_size; cs->obj_bytes = NULL; } else { cs->is_nobits = 0; diff --git a/test/extlink/run.sh b/test/extlink/run.sh @@ -0,0 +1,135 @@ +#!/bin/sh +# test/extlink/run.sh — external-linker linkability harness (Type-K mode-P). +# +# Proves that objects emitted by `kit cc` are consumable by the *system* +# linkers, not just kit's own `ld`: LLVM lld in its three personalities +# (ld.lld / lld-link / ld64.lld) plus Apple's ld on macOS. One lane per +# (object-format, linker); a lane is SKIPPED (never failed) when its linker +# or a required SDK is missing, so the suite is portable across CI hosts. +# +# Regression guard for two fixes: +# - COFF: .bss size must live in SizeOfRawData (not VirtualSize), else lld +# discards the section -> "relocation against symbol in discarded section". +# - Mach-O: kit no longer emits a raw __eh_frame whose FDE pc-begin reloc +# ld64 / ld64.lld reject ("UNSIGNED relocation must not be PC-relative"). +# +# The corpus is two TUs that reference each other across a call and a shared +# global (so the link exercises both a branch reloc and a data reloc). + +set -u + +script_dir=$(cd "$(dirname "$0")" && pwd) +repo_root=$(cd "$script_dir/../.." && pwd) +KIT_KIT_DIR="$repo_root/test/lib" +. "$repo_root/test/lib/kit_sh_kit.sh" + +KIT="${KIT:-$repo_root/build/kit}" +export KIT +kit_require_kit extlink +kit_workdir extlink +work="$KIT_WORK" +kit_report_init + +cat > "$work/a.c" <<'EOF' +extern int kx_b(int); +int kx_g = 7; +int kx_a(void) { return kx_b(kx_g) + kx_g; } +EOF +cat > "$work/b.c" <<'EOF' +extern int kx_g; +int kx_b(int x) { return x + kx_g; } +EOF + +# compile TARGET EXT : build a.<EXT> and b.<EXT> for TARGET (empty = native). +# Returns nonzero (with the diagnostic in cc.err) if kit cc fails. +compile() { + _t=$1 + _ext=$2 + "$KIT" cc ${_t:+-target "$_t"} -O0 -c "$work/a.c" -o "$work/a.$_ext" \ + 2>"$work/cc.err" && + "$KIT" cc ${_t:+-target "$_t"} -O0 -c "$work/b.c" -o "$work/b.$_ext" \ + 2>>"$work/cc.err" +} + +# ---- ELF: ld.lld -------------------------------------------------------- +if command -v ld.lld >/dev/null 2>&1; then + for t in aarch64-linux x86_64-linux riscv64-linux; do + if compile "$t" o; then + run_ok "elf-ld.lld-$t" ld.lld -static -no-pie -e kx_a \ + -o "$work/$t.elf" "$work/a.o" "$work/b.o" + is_executable "elf-ld.lld-$t-exe" "$work/$t.elf" + else + not_ok "elf-ld.lld-$t" "$work/cc.err" + fi + done +else + skip_test "elf-ld.lld" "ld.lld not on PATH" +fi + +# ---- COFF: lld-link ----------------------------------------------------- +if command -v lld-link >/dev/null 2>&1; then + for t in x86_64-windows aarch64-windows; do + if compile "$t" obj; then + run_ok "coff-lld-link-$t" lld-link /dll /noentry \ + /export:kx_a /export:kx_b \ + "/out:$work/$t.dll" "$work/a.obj" "$work/b.obj" + else + not_ok "coff-lld-link-$t" "$work/cc.err" + fi + done +else + skip_test "coff-lld-link" "lld-link not on PATH" +fi + +# ---- Mach-O: Apple ld + ld64.lld (macOS, Apple Silicon) ----------------- +# Needs an SDK (libSystem + crt), so gate on xcrun. x86_64-macOS has a +# separate, still-open __text addend-reloc bug, so restrict the native lane +# to arm64 hosts (see doc note); link+run a real main() through the clang +# driver, exercising the default Apple ld and lld end to end. +machos_skip="" +if ! command -v xcrun >/dev/null 2>&1; then + machos_skip="no macOS SDK (xcrun)" +elif [ "$(uname -m 2>/dev/null)" != "arm64" ]; then + machos_skip="x86_64-macOS __text reloc fix pending" +elif ! command -v clang >/dev/null 2>&1; then + machos_skip="clang not on PATH" +fi +if [ -n "$machos_skip" ]; then + skip_test "macho" "$machos_skip" +else + SDK=$(xcrun --show-sdk-path 2>/dev/null || echo) + cat > "$work/m.c" <<'EOF' +extern int kx_helper(int); +int main(void) { return kx_helper(21); } /* expect exit 42 */ +EOF + cat > "$work/mh.c" <<'EOF' +int kx_g2 = 0; +int kx_helper(int x) { kx_g2 = x; return kx_g2 * 2; } +EOF + if "$KIT" cc -O0 -c "$work/m.c" -o "$work/m.o" 2>"$work/cc.err" && + "$KIT" cc -O0 -c "$work/mh.c" -o "$work/mh.o" 2>>"$work/cc.err"; then + # Apple ld (clang's default linker). + if clang -isysroot "$SDK" -o "$work/m_apple" "$work/m.o" "$work/mh.o" \ + 2>"$work/apple.err"; then + "$work/m_apple" 2>/dev/null + if [ $? -eq 42 ]; then ok "macho-apple-ld" + else echo "wrong exit code" >"$work/apple.err"; not_ok "macho-apple-ld" "$work/apple.err"; fi + else + not_ok "macho-apple-ld" "$work/apple.err" + fi + # ld64.lld. + if clang -fuse-ld=lld -isysroot "$SDK" -o "$work/m_lld" \ + "$work/m.o" "$work/mh.o" 2>"$work/lld.err"; then + "$work/m_lld" 2>/dev/null + if [ $? -eq 42 ]; then ok "macho-ld64.lld" + else echo "wrong exit code" >"$work/lld.err"; not_ok "macho-ld64.lld" "$work/lld.err"; fi + else + not_ok "macho-ld64.lld" "$work/lld.err" + fi + else + not_ok "macho-compile" "$work/cc.err" + fi +fi + +kit_summary extlink +kit_exit diff --git a/test/objcopy/cases/01-rename-section.expected b/test/objcopy/cases/01-rename-section.expected @@ -1,2 +1 @@ __TEXT,__mytext -__TEXT,__eh_frame diff --git a/test/objcopy/cases/04-add-section.expected b/test/objcopy/cases/04-add-section.expected @@ -1,3 +1,2 @@ __DATA,__custom -__TEXT,__eh_frame __TEXT,__text diff --git a/test/strip/cases/01-strip-debug.expected b/test/strip/cases/01-strip-debug.expected @@ -1,5 +1,4 @@ == sections == -__TEXT,__eh_frame __TEXT,__text == symbols == _helper diff --git a/test/strip/cases/02-strip-all-keeps-reloc-targets.expected b/test/strip/cases/02-strip-all-keeps-reloc-targets.expected @@ -1,6 +1,4 @@ == symbols == _helper -_main == sections == -__TEXT,__eh_frame __TEXT,__text diff --git a/test/strip/cases/03-keep-symbol.expected b/test/strip/cases/03-keep-symbol.expected @@ -1,4 +1,3 @@ == symbols == _helper -_main _unused diff --git a/test/strip/cases/04-archive-strip-debug.expected b/test/strip/cases/04-archive-strip-debug.expected @@ -2,8 +2,6 @@ a.o b.o == a.o sections == -__TEXT,__eh_frame __TEXT,__text == b.o sections == -__TEXT,__eh_frame __TEXT,__text