kit

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

commit ded0e181e2962ef600e6fe13935f4fb822365e7a
parent a3638ac36240b70aea3f2734221fb3dfef504f6a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 12:04:17 -0700

test-elf: scope to ELF fidelity only; fix test-link R-path no-op

test/elf/ is now strictly object-file roundtrip. Layer D (exec) and
Layer B's run-step are deleted — every case had a richer equivalent in
test/link/cases/, and the run-step was broken-by-design (12/13 Layer B
cases have no main).

Also drop the xfail mechanism: failures fail. 06_tls is now a real
failure pending TLS-DESC reloc support in elf_read.c.

test/link/run.sh was invoking python3 normalize.py with no argument,
which silently writes empty output — the R-path structural diff was a
no-op since inception. Pass 'filter' so it actually canonicalizes.

Diffstat:
Mdoc/linker-status.md | 219+++++++++++++++++++++++++++++++++++--------------------------------------------
Dtest/elf/cases/06_tls.xfail | 5-----
Dtest/elf/exec/.gitkeep | 0
Dtest/elf/exec/01_exit_0.c | 16----------------
Dtest/elf/exec/02_exit_42.c | 15---------------
Dtest/elf/exec/03_call_local.c | 22----------------------
Dtest/elf/exec/04_load_rodata.c | 18------------------
Dtest/elf/exec/05_load_data.c | 18------------------
Dtest/elf/exec/06_bss.c | 18------------------
Dtest/elf/exec/07_two_tus_a.c | 18------------------
Dtest/elf/exec/07_two_tus_b.c | 3---
Dtest/elf/exec/08_weak_sym_a.c | 22----------------------
Dtest/elf/exec/08_weak_sym_b.c | 3---
Dtest/elf/exec/09_common_sym.cflags | 1-
Dtest/elf/exec/09_common_sym_a.c | 24------------------------
Dtest/elf/exec/09_common_sym_b.c | 6------
Dtest/elf/exec/10_init_array.c | 31-------------------------------
Dtest/elf/exec/11_init_array_order.c | 42------------------------------------------
Mtest/elf/run.sh | 288++++---------------------------------------------------------------------------
Mtest/link/run.sh | 4++--
20 files changed, 113 insertions(+), 660 deletions(-)

diff --git a/doc/linker-status.md b/doc/linker-status.md @@ -1,149 +1,124 @@ -# Linker / JIT status — `make test-link` +# Linker / JIT / ELF status -## Test score +Tracks the three behavioral harnesses that share the link + obj surface: -| State | Pass | Fail | Skip | -|-------|------|------|------| -| Before session 1 | 0 | 0 | 88 (detection bug) | -| End of session 1 (claimed, unverified) | 66 | 20 | 0 | -| Current J-path baseline (clean rebuild) | ~19 | ~11 | — | -| Target | 88 | 0 | 0 | +- `make test-elf` — ELF object-file fidelity (read / write / roundtrip). +- `make test-link` — link + JIT (R/E/J paths per case). +- `make test-cg` — codegen + JIT (D/R/E/J paths per case). -> J-path-only counts above (one path per case, run on Apple Silicon host). -> R and E paths not measured this session — E requires qemu/podman per case -> and was too slow to iterate against; R is gated on the harness binaries -> rebuilding correctly, which session 1 left broken. +`test-elf` is **strictly object-file fidelity**. Linker and exe behavior +live in `test/link/` — they are not duplicated in `test/elf/`. The old +Layer D (exec) and Layer B run-step were removed because every case had +a richer equivalent in `test/link/cases/`. --- -## Session 2 changes +## test-elf status -### Build system: stale artifacts no longer mask correctness +| Layer | Source | State | +|-------|--------|-------| +| A — unit | `test/elf/unit/*.c` | smoke passes | +| B — clang-oracle structural diff | `test/elf/cases/*.c` | 12/13 pass; `06_tls` fails (real) | +| C — bad ELF | `test/elf/bad/*.elf` | (no inputs yet) | -The session-1 "JIT lookup returns NULL for all cases" blocker turned out to -be a build-staleness artifact, not a real bug. After `make clean && make -lib`, JIT lookup works correctly — the symhash code was always fine. The -session lost time chasing this because the Makefile's dependency tracking -was incomplete. +### Layer B — what's broken and why -Fixes: +| Case | Symptom | Root cause | +|------|---------|------------| +| `06_tls` | roundtrip rejected: "unsupported AArch64 reloc type 549" | TLS-DESC reloc family not implemented in `elf_read.c`. Either implement the read side as a passthrough or add real TLS support. | -| Fix | Why | -|-----|-----| -| `Makefile`: `-MMD -MP` on every compile + `-include $(DEPS)` | Touching a header now rebuilds dependent .o files. Without this, `link.h` / `link_internal.h` edits silently produced mixed-version archives. | -| `Makefile`: `rm -f $(LIB_AR)` before `ar rcs` | `ar rcs` is additive; deleted .c files would otherwise leave stale .o entries in the archive. | -| `test/test.mk`: `cfree-roundtrip`, `link-exe-runner`, `jit-runner` declared as Make targets with `$(LIB_AR)` prerequisite | `test-link` now rebuilds the harness automatically when libcfree changes. The previous `run.sh`-driven build skipped `cfree-roundtrip` entirely (only `test-elf` rebuilt it). | -| `test/link/run.sh`, `test/elf/run.sh`: locate (not build) harness binaries | Single source of truth for the rebuild rule lives in the Makefile. | -| `src/api/pipeline.c`: add missing `R_AARCH64_JUMP26`, `R_AARCH64_ADR_GOT_PAGE`, `R_AARCH64_LD64_GOT_LO12_NC` cases to `reloc_kind_name` switch | `-Werror=switch` broke `make clean && make lib` until the new RelocKind enum entries were handled. | +Previously-broken cases that are now passing: -Verification: -- `touch src/link/link.h && make lib` rebuilds 7 source files (was: 0). -- `touch src/link/link.h && make build/test/jit-runner` chains through libcfree to the harness. +- `05_common_sym` — fixed: `elf_emit.c:sym_kind_to_elf` now emits `STT_OBJECT` + `shndx=SHN_COMMON` (was wrongly emitting `STT_COMMON`). +- `09_ifunc` — fixed (OS/ABI emit + `STT_GNU_IFUNC` round-trip). +- `13_comdat` — fixed (`SHT_GROUP` signature symbol preserved; `.eh_frame` alignment). -### J-path baseline after clean rebuild +### Layer B — assertion mechanism -After a fully clean build (`make clean && make test-link`), the J-path -test scores fluctuate between roughly 18 and 23 passing depending on -intermittent timeouts (see Blocker 0). +Per case: -Stable failures (always fail, expected feature gaps): +``` +clang --target=aarch64-linux-gnu -c case.c -> golden.o +cfree-roundtrip golden.o -> rt.o +python3 test/elf/normalize.py readelf golden.o > golden.readelf +python3 test/elf/normalize.py readelf rt.o > rt.readelf +diff -u golden.readelf rt.readelf # pass = empty diff +``` -| Case | Symptom | Cause | -|------|---------|-------| -| `14_weak_present`, `16_weak_undef` | exit 1 | static GOT not implemented | -| `25_gc_sections` | exit 1 | `--gc-sections` accepted but ignored | -| `26_archive_demand`, `27_archive_whole` | exit 1 | `link_add_archive_bytes` panics | +`normalize.py` collapses layout-dependent details (addresses → `<addr>`, +indices → `<idx>`, sorts symbol/reloc/section blocks, strips per-block +counts/offsets, drops `.llvm_addrsig`). The contract the harness +enforces is byte-equality of the *normalized* readelf dumps. -Intermittent failures (the real new blocker): - -| Cases | Symptom | -|-------|---------| -| `02`, `04`, `05`, `06`, `07`, `09`, `13`, `15`, `18`, `20`, `22`, `23`, `24`, `27`, `29`, ... | exit 124 (5-second timeout). Different cases hang on different runs. Even `29_jit_lookup_miss` (just `return 0;`) sometimes hangs. Stress-running `04_rodata_u32` 20 times in a loop reproduces ~30% hang rate. | +There is **no xfail mechanism** — failures fail. If a case is broken, +fix the bug or remove the case. --- -## Blockers (in priority order) - -### 0 — Intermittent JIT timeouts (NEW, blocks everything else) - -Cases with no flow-control of their own (e.g., `04_rodata_u32` reads a -const, returns 0 or 1) hang ~30% of the time. Different cases hang on -different runs, including trivially-correct cases like `29_jit_lookup_miss`. - -**Hypotheses to investigate** (in order of likelihood): - -1. **icache / dcache coherency on Apple Silicon.** `cfree_jit_from_image` - calls `__builtin___clear_cache(base, base + map_size)` *before* - `mprotect`. On ARMv8 the standard sequence is `dc cvau; dsb ish; ic - ivau; dsb ish; isb`, and the `isb` should follow the permission flip, - not precede it. Try moving the flush to after mprotect, or add a - manual `__asm__("isb")` after each `mprotect`. -2. **macOS hardened-runtime / `MAP_JIT`.** mmap'd RW pages flipped to RX - via `mprotect` work without `MAP_JIT` for unsigned binaries, but the - transition may be racy. Try `mmap(..., MAP_JIT, ...)` and - `pthread_jit_write_protect_np` instead of mprotect. -3. **Address-space layout interaction.** ASLR varies the mmap base. Some - bases may produce ADRP encodings that look correct but interact badly - with the BTI / PAC defaults on Apple Silicon. Check whether the hang - correlates with specific high bits of `jit->base`. - -**Next step**: when a hang reproduces, attach `lldb` (or `sample(1)`) to -the hung pid and capture the PC. If PC is inside `test_main`, the relocs -landed wrong. If PC is at the entry but executing stale instructions, -it's a cache-coherency issue. - -### 1 — Static GOT (cases 14, 15, 16) - -Unchanged from session 1 plan. `weak_present`, `weak_override`, -`weak_undef` use GOT-indirect relocs even with `-fno-PIC` because clang -always GOT-routes weak externs. - -**Design**: -- During `emit_reloc_records`, collect symbols referenced via - `R_AARCH64_ADR_GOT_PAGE` / `R_AARCH64_LD64_GOT_LO12_NC`. Record unique - GOT-needing symbol ids. -- After `layout_sections` / `layout_commons`, append a synthetic 8-byte - `.got` section to the RW segment. Store GOT-slot vaddrs in a - `got_vaddr[]` array indexed by `LinkSymId`. -- In `link_reloc_apply`, `ADR_GOT_PAGE` uses the GOT slot's page - address; `LD64_GOT_LO12_NC` uses its page-aligned offset. GOT slots - hold the symbol's final address (0 for undefined weak). - -**Cleanup once GOT lands**: drop `-fno-PIC -fno-pie` from -`test/link/run.sh` (both test source and `start.c` lines). - -### 2 — Archive loading (cases 26, 27) - -Unchanged. `cfree_ar_iter` already exists in `src/api/ar.c`. Wire it up -in `link_add_archive_bytes`: - -- **`whole_archive`**: iterate all members, `link_add_obj_bytes` each. -- **demand-loading**: build a name→member-offset index from the `/` - member; after `resolve_symbols` identifies undefs, pull the member - defining each undef, re-run resolution, repeat until stable. Needs a - "any undefs remaining?" query in the resolve path. - -### 3 — GC sections (case 25) - -Unchanged. Before `layout_sections`, BFS from the entry symbol (and -SSEM_INIT_ARRAY / SSEM_FINI_ARRAY roots) over the reloc graph; mark -sections that aren't reached and drop them in `section_kept`. +## test-link / JIT — Apple Silicon J-path + +The intermittent JIT hangs documented as "Blocker 0" in the previous +revision of this doc were **W^X violations on Apple Silicon's hardened +runtime**. Without `MAP_JIT`, mapping a region RW then flipping to RX +via `mprotect` is undefined behavior on Apple Silicon — sometimes it +works, sometimes the next instruction fetch traps and the process spins +or aborts. + +### Fix in this session + +`driver/env.c` (host execmem): + +- Apple Silicon path now uses `mmap(..., MAP_PRIVATE | MAP_ANON | MAP_JIT, ...)` for any region whose final perms include `PROT_EXEC`. Data/RO segments stay on plain anonymous mappings so JITed code can still write to them. +- `pthread_jit_write_protect_np(0)` is asserted while the region is being populated; the `protect()` and `flush_icache()` paths flip it back to `1` (exec mode) before control transfers. + +`src/link/link_jit.c` (per-segment maps): + +- `CfreeJit` no longer holds a single `(base, map_size)`. A `JitSegMap` per `LinkSegment` lets each segment carry its own host reservation with its own perms hint, which is what makes selective `MAP_JIT` possible (you can't have part of a single mapping be MAP_JIT and part not). +- Reloc apply and `cfree_jit_lookup` resolve image-vaddr → runtime address through `vaddr_to_runtime(img, segs, vaddr)` (linear scan; up to 3 segments). +- `flush_icache` is now called per code segment after the protect flip, which is also the finalize point on Apple where the thread W^X bit must be left in exec mode. + +The host requires the `com.apple.security.cs.allow-jit` entitlement when +MAP_JIT is in play. For ad-hoc dev: +`codesign -s - --entitlements jit.plist <bin>`. + +Not yet measured against the full `test-link` J-path — needs a clean +sweep with the new per-segment layout. + +### R-path normalizer fix + +`test/link/run.sh` was invoking `python3 normalize.py` with **no +argument** for the R-path structural diff (lines 240, 242). With no +arg, normalize.py prints its docstring to stderr and writes nothing to +stdout — both `*_golden.norm` and `*_rt.norm` were empty, so `diff -u` +trivially passed. The R path was a silent no-op. Fixed by passing +`filter` (read stdin → write normalized stdout). R-path failures may +now surface for the first time. --- -## fini_array note (was Blocker 2) +## Remaining todos (rough priority) + +### test-elf -Session 1's listed fini_array SIGSEGV blocker (cases 21/22/23) does not -reproduce after the build-staleness fix; those cases pass cleanly when -they don't hit the intermittent timeout from Blocker 0. Removed from -the priority list. +1. **`06_tls`** — implement TLS reloc read (at minimum opaque-passthrough so non-TLS cases that share TUs aren't blocked). +2. **`test/elf/bad/`** — populate Layer C with malformed ELFs + `.expect` strings (the harness exists; the corpus does not). +3. **Grow Layer A** per `test/elf/CORPUS.md` for ELF edges clang won't naturally emit (ELFCLASS32, ELFOSABI variants, big-endian, custom `sh_type`s). + +### test-link / JIT (post-MAP_JIT) + +1. Re-run J-path full sweep; confirm Blocker 0 hangs are gone. +2. Audit R-path failures now that the normalizer is actually running (was previously hidden). +3. Static GOT — cases `14_weak_present`, `15`, `16_weak_undef` (design notes preserved from prior session: collect GOT-needing symbols at `emit_reloc_records`; append synthetic `.got` to RW segment; resolve in `link_reloc_apply`). +4. Archive loading — cases `26_archive_demand`, `27_archive_whole`. Wire `cfree_ar_iter` into `link_add_archive_bytes`. +5. `--gc-sections` — case `25`. --- -## Dependency order +## Build hygiene (from prior session, still load-bearing) + +- `Makefile` uses `-MMD -MP` so header edits force dependents to rebuild. +- `ar rcs` is preceded by `rm -f $(LIB_AR)` so deleted .c files don't leave stale .o entries in the archive. +- `cfree-roundtrip`, `link-exe-runner`, `jit-runner` are Make targets with `$(LIB_AR)` as a prerequisite — `run.sh` *locates* them, never *builds* them. -1. **Intermittent timeouts (Blocker 0)** — until cases run deterministically - the other fixes can't be measured. -2. **GOT** → unblocks 14/15/16 (and lets the `-fno-PIC` workaround go). -3. **Archive loading** → unblocks 26/27. -4. **GC sections** → unblocks 25. +If a test result looks impossible given the source, suspect staleness +first (`make clean && make lib && bash test/elf/run.sh <case>`). diff --git a/test/elf/cases/06_tls.xfail b/test/elf/cases/06_tls.xfail @@ -1,5 +0,0 @@ -TLS-DESC reloc family (e.g. R_AARCH64_TLSDESC_ADR_PAGE21=549) not yet -implemented in elf_read.c / elf_emit.c. Roundtrip rejects the input -during read with "unsupported AArch64 reloc type 549". TLS is its own -milestone (see doc/linker-status.md); remove this .xfail when TLS-DESC -relocs are supported on both sides. diff --git a/test/elf/exec/.gitkeep b/test/elf/exec/.gitkeep diff --git a/test/elf/exec/01_exit_0.c b/test/elf/exec/01_exit_0.c @@ -1,16 +0,0 @@ -/* Layer D: minimum-viable exec — _start exits 0. - * - * No relocations: validates ehdr/phdr/segment plumbing and e_entry. */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - sys_exit(0); -} diff --git a/test/elf/exec/02_exit_42.c b/test/elf/exec/02_exit_42.c @@ -1,15 +0,0 @@ -/* Layer D: same as 01 but with a non-zero exit code, to catch - * accidental hard-coding of zero somewhere in the e_entry path. */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - sys_exit(42); -} diff --git a/test/elf/exec/03_call_local.c b/test/elf/exec/03_call_local.c @@ -1,22 +0,0 @@ -/* Layer D: in-section call to exercise R_AARCH64_CALL26. - * - * `_start` calls a sibling function in the same .text — clang - * lowers that to BL with a R_AARCH64_CALL26 relocation. */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -static int __attribute__((noinline)) double_it(int x) -{ - return x + x; -} - -void _start(void) -{ - sys_exit(double_it(7)); /* exits 14 */ -} diff --git a/test/elf/exec/04_load_rodata.c b/test/elf/exec/04_load_rodata.c @@ -1,18 +0,0 @@ -/* Layer D: load a rodata constant via ADRP + ADD + LDR. Exercises - * R_AARCH64_ADR_PREL_PG_HI21 + R_AARCH64_ADD_ABS_LO12_NC across - * segments (text -> rodata). */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -static const int answer = 17; - -void _start(void) -{ - sys_exit(answer); -} diff --git a/test/elf/exec/05_load_data.c b/test/elf/exec/05_load_data.c @@ -1,18 +0,0 @@ -/* Layer D: load a writable .data int. Exercises the R+W segment plus - * the same ADRP/ADD pair across text -> data. */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -static int counter = 11; - -void _start(void) -{ - counter += 1; - sys_exit(counter); /* exits 12 */ -} diff --git a/test/elf/exec/06_bss.c b/test/elf/exec/06_bss.c @@ -1,18 +0,0 @@ -/* Layer D: BSS — declare zero-initialised int, write, read, exit. - * Tests segment.mem_size > file_size and zero-on-load. */ - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -static int x; /* BSS */ - -void _start(void) -{ - x = 7; - sys_exit(x); -} diff --git a/test/elf/exec/07_two_tus_a.c b/test/elf/exec/07_two_tus_a.c @@ -1,18 +0,0 @@ -/* Layer D, multi-TU: TU A defines `_start`, calls `add` from TU B, and - * exits with the result. Tests cross-TU SB_GLOBAL resolution - * (R_AARCH64_CALL26 against a definition in another input). */ - -extern int add(int, int); - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - sys_exit(add(20, 22)); /* exits 42 */ -} diff --git a/test/elf/exec/07_two_tus_b.c b/test/elf/exec/07_two_tus_b.c @@ -1,3 +0,0 @@ -/* Layer D, multi-TU: TU B defines the global `add` referenced by TU A. */ - -int add(int x, int y) { return x + y; } diff --git a/test/elf/exec/08_weak_sym_a.c b/test/elf/exec/08_weak_sym_a.c @@ -1,22 +0,0 @@ -/* Layer D, multi-TU: weak-symbol resolution. - * - * TU A provides a weak definition of get_val() (returns 1) and _start. - * TU B provides a strong definition of get_val() (returns 42). - * The linker must pick the strong definition; expected exit code: 42. */ - -extern int get_val(void); - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; /* __NR_exit */ - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -__attribute__((weak)) int get_val(void) { return 1; } /* loses to TU B */ - -void _start(void) -{ - sys_exit(get_val()); /* expects 42 */ -} diff --git a/test/elf/exec/08_weak_sym_b.c b/test/elf/exec/08_weak_sym_b.c @@ -1,3 +0,0 @@ -/* Layer D, multi-TU: strong definition that overrides TU A's weak one. */ - -int get_val(void) { return 42; } diff --git a/test/elf/exec/09_common_sym.cflags b/test/elf/exec/09_common_sym.cflags @@ -1 +0,0 @@ --fcommon diff --git a/test/elf/exec/09_common_sym_a.c b/test/elf/exec/09_common_sym_a.c @@ -1,24 +0,0 @@ -/* Layer D, multi-TU: COMMON symbol coalescing. - * - * Both TUs declare `int shared` without an initialiser (-fcommon). - * The linker allocates exactly one copy. TU A writes 42 to shared, - * then calls read_shared() from TU B which reads the same storage. - * Expected exit code: 42. */ - -int shared; /* SHN_COMMON */ - -extern int read_shared(void); - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - shared = 42; - sys_exit(read_shared()); /* expects 42 */ -} diff --git a/test/elf/exec/09_common_sym_b.c b/test/elf/exec/09_common_sym_b.c @@ -1,6 +0,0 @@ -/* Layer D, multi-TU: second tentative definition of `shared`. - * Merged with TU A's COMMON by the linker into a single BSS allocation. */ - -int shared; - -int read_shared(void) { return shared; } diff --git a/test/elf/exec/10_init_array.c b/test/elf/exec/10_init_array.c @@ -1,31 +0,0 @@ -/* Layer D: SHT_INIT_ARRAY — constructor runs before _start reads the flag. - * - * There is no libc in this freestanding binary, so _start must manually - * walk __init_array_start / __init_array_end (provided by the linker). - * This tests that the linker emits a well-formed .init_array section with - * a valid relocation pointing at ctor(). - * - * Expected exit code: 1 (ctor set init_ran=1 before _start read it). */ - -static int init_ran = 0; - -__attribute__((constructor)) static void ctor(void) { init_ran = 1; } - -typedef void (*init_fn_t)(void); -extern init_fn_t __init_array_start[]; -extern init_fn_t __init_array_end[]; - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - for (init_fn_t *f = __init_array_start; f < __init_array_end; f++) - (*f)(); - sys_exit(init_ran); /* 1 = ctor ran; 0 = init_array not called */ -} diff --git a/test/elf/exec/11_init_array_order.c b/test/elf/exec/11_init_array_order.c @@ -1,42 +0,0 @@ -/* Layer D: SHT_INIT_ARRAY priority ordering. - * - * Two constructors with explicit priorities: 101 and 102. Lower numbers - * run first. ctor_101 sets `order` to 1 iff it sees 0; ctor_102 sets - * `order` to 42 iff it sees 1. Correct ordering yields exit code 42. - * - * Sections emitted: .init_array.00101, .init_array.00102. - * The linker must sort them ascending before placing in the output. */ - -static int order = 0; - -__attribute__((constructor(101))) static void ctor_101(void) -{ - if (order == 0) order = 1; /* fires first: 0 → 1 */ -} - -__attribute__((constructor(102))) static void ctor_102(void) -{ - if (order == 1) order = 42; /* fires second: 1 → 42 */ -} - -typedef void (*init_fn_t)(void); -extern init_fn_t __init_array_start[]; -extern init_fn_t __init_array_end[]; - -static void __attribute__((noreturn)) sys_exit(long code) -{ - register long x0 __asm__("x0") = code; - register long x8 __asm__("x8") = 93; - __asm__ volatile("svc #0" :: "r"(x0), "r"(x8)); - __builtin_unreachable(); -} - -void _start(void) -{ - for (init_fn_t *f = __init_array_start; f < __init_array_end; f++) - (*f)(); - /* 42 = both ctors ran in correct order - * 1 = ctor_102 ran first (wrong) - * 0 = neither ran */ - sys_exit(order); -} diff --git a/test/elf/run.sh b/test/elf/run.sh @@ -1,24 +1,25 @@ #!/usr/bin/env bash -# test/elf/run.sh — top-level driver for ELF roundtrip tests. +# test/elf/run.sh — ELF object-file fidelity tests. +# +# Scope: read/write/roundtrip of relocatable ELF .o files only. Linker +# and exe behavior live in test/link/ — do not duplicate them here. # # Layers (each runs only when its prerequisites are present): # # A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests. # Each .c links against build/libcfree.a, runs, # and exits 0 on success. -# B. test/elf/cases/*.c clang-oracle behavioral tests: +# B. test/elf/cases/*.c clang-oracle structural roundtrip tests: # clang -c case.c -> golden.o # cfree-roundtrip golden.o -> rt.o -# clang ... -> golden.exe / rt.exe -# diff stdout/stderr/exit + structural diff. +# normalized readelf diff golden.o vs rt.o. # C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip # to fail with a substring from <name>.expect. # -# Tools detected: clang, llvm-readelf, llvm-objdump, qemu-aarch64-static, -# python3. Missing tools cause the dependent layer to be skipped, not -# failed. Set CFREE_TEST_ALLOW_SKIP=1 to allow the harness to exit 0 with -# skips; otherwise any skip makes the run fail (so CI catches a silently -# degraded run). +# Tools detected: clang, llvm-readelf, python3. Missing tools cause the +# dependent layer to be skipped, not failed. Set CFREE_TEST_ALLOW_SKIP=1 +# to allow the harness to exit 0 with skips; otherwise any skip makes +# the run fail (so CI catches a silently degraded run). set -u @@ -48,68 +49,15 @@ note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$ have_clang=0 have_llvm_readelf=0 -have_llvm_objdump=0 -have_qemu=0 -have_podman=0 have_python3=0 command -v clang >/dev/null 2>&1 && have_clang=1 command -v llvm-readelf >/dev/null 2>&1 && have_llvm_readelf=1 command -v readelf >/dev/null 2>&1 && have_llvm_readelf=1 -command -v llvm-objdump >/dev/null 2>&1 && have_llvm_objdump=1 -command -v objdump >/dev/null 2>&1 && have_llvm_objdump=1 -command -v qemu-aarch64-static >/dev/null 2>&1 && have_qemu=1 -command -v qemu-aarch64 >/dev/null 2>&1 && have_qemu=1 command -v python3 >/dev/null 2>&1 && have_python3=1 -command -v podman >/dev/null 2>&1 && have_podman=1 -QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || echo)" READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || echo)" -# AArch64 binary runner. Layer B (clang-oracle behavioral diff) and Layer D -# (cfree ld behavioral diff) need to execute aarch64 ELF binaries. On Linux -# hosts qemu-aarch64-static does this directly; on macOS we shell out to -# podman, which transparently runs the binary in a Linux ARM64 container -# (native on Apple Silicon, qemu-emulated on x86 macs). The image is -# selected via $RUN_AARCH64_IMAGE (default alpine:latest); a podman machine -# must already be running. -# -# Usage: run_aarch64 <exe-abs-path> <stdout-file> <stderr-file> -# Sets: RUN_RC exit code returned by the binary (or runner failure code) -RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" -have_runner=0 -if [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; then - have_runner=1 -fi - -run_aarch64() { - local exe="$1" out="$2" err="$3" - if [ $have_qemu -eq 1 ]; then - "$QEMU_BIN" "$exe" > "$out" 2> "$err" - RUN_RC=$? - return - fi - if [ $have_podman -eq 1 ]; then - local dir base - dir="$(cd "$(dirname "$exe")" && pwd)" - base="$(basename "$exe")" - # --net=none keeps the runs hermetic; -w /work + ./binary so that - # static-nostdlib binaries do not depend on $PATH lookup. - podman run --rm --platform linux/arm64 --net=none \ - -v "$dir":/work:Z -w /work \ - "$RUN_AARCH64_IMAGE" "./$base" > "$out" 2> "$err" - RUN_RC=$? - return - fi - RUN_RC=127 -} - -runner_label() { - if [ $have_qemu -eq 1 ]; then echo "qemu ($QEMU_BIN)"; return; fi - if [ $have_podman -eq 1 ]; then echo "podman ($RUN_AARCH64_IMAGE)"; return; fi - echo "none" -} - # ----- locate cfree-roundtrip ------------------------------------------- # Built as a Make target (test/test.mk) so it picks up libcfree.a changes. @@ -122,10 +70,6 @@ roundtrip_ok=0 printf 'cfree ELF test harness\n' printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)" printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)" -printf ' llvm-objdump: %s\n' "$([ $have_llvm_objdump -eq 1 ] && echo yes || echo no)" -printf ' qemu-aarch64: %s\n' "$([ $have_qemu -eq 1 ] && echo yes || echo no)" -printf ' podman: %s\n' "$([ $have_podman -eq 1 ] && echo yes || echo no)" -printf ' aarch64 runner: %s\n' "$(runner_label)" printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)" printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo found || echo "MISSING — run \"make $ROUNDTRIP_BIN\"")" printf '\n' @@ -185,12 +129,6 @@ else wd="$BUILD_DIR/$stem" mkdir -p "$wd" - # Honor an optional ".xfail" sentinel beside the .c — for cases that - # exercise reloc kinds not yet supported. xfail-cases must FAIL until - # the kind lands; if they start passing, that is a hard failure. - xfail=0 - [ -f "${src%.c}.xfail" ] && xfail=1 - # Per-case extra compiler flags: drop a NN_name.cflags file alongside # the .c to pass additional flags (e.g. -ffunction-sections, -x c++). extra_cflags="" @@ -204,16 +142,9 @@ else continue fi - rt_ok=1 - "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log" || rt_ok=0 - - if [ $rt_ok -ne 1 ]; then - if [ $xfail -eq 1 ]; then - note_pass "$name (xfail: roundtrip rejected)" - else - note_fail "$name (roundtrip failed)" - sed 's/^/ | /' "$wd/roundtrip.log" - fi + if ! "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log"; then + note_fail "$name (roundtrip failed)" + sed 's/^/ | /' "$wd/roundtrip.log" continue fi @@ -221,49 +152,12 @@ else python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then - if [ $xfail -eq 1 ]; then - note_pass "$name (xfail: structural diff differs)" - continue - fi note_fail "$name (readelf diff)" head -40 "$wd/readelf.diff" | sed 's/^/ | /' continue fi - # Behavioral diff requires an aarch64 runner (qemu or podman). - if [ $have_runner -eq 1 ]; then - if ! clang --target=aarch64-linux-gnu -static "$wd/golden.o" -o "$wd/golden.exe" \ - 2> "$wd/link_golden.log"; then - note_skip "$name (run)" "linking golden.exe failed" - continue - fi - if ! clang --target=aarch64-linux-gnu -static "$wd/rt.o" -o "$wd/rt.exe" \ - 2> "$wd/link_rt.log"; then - note_fail "$name (run): linking rt.exe failed" - continue - fi - run_aarch64 "$wd/golden.exe" "$wd/golden.out" "$wd/golden.err"; ge=$RUN_RC - run_aarch64 "$wd/rt.exe" "$wd/rt.out" "$wd/rt.err"; re=$RUN_RC - cat "$wd/golden.err" >> "$wd/golden.out" - cat "$wd/rt.err" >> "$wd/rt.out" - echo $ge >> "$wd/golden.out" - echo $re >> "$wd/rt.out" - if ! diff -u "$wd/golden.out" "$wd/rt.out" > "$wd/run.diff"; then - if [ $xfail -eq 1 ]; then - note_pass "$name (xfail: run output differs)" - continue - fi - note_fail "$name (run output)" - head -40 "$wd/run.diff" | sed 's/^/ | /' - continue - fi - fi - - if [ $xfail -eq 1 ]; then - note_fail "$name (XPASS — case marked xfail but everything matched; remove .xfail)" - else - note_pass "$name" - fi + note_pass "$name" done fi printf '\n' @@ -311,160 +205,6 @@ else fi printf '\n' -# ----- Layer D: exec/*.c — exe behavioral comparison -------------------- -# -# Case grouping: all .c files sharing the same leading [0-9]+ prefix form -# ONE test case. They are compiled to separate .o files and linked -# together (multi-TU). Single-file cases keep working unchanged. -# -# Layout convention: -# exec/01_exit_0.c -> case "01_exit_0" (1 TU) -# exec/07_multi_a.c + 07_multi_b.c -> case "07_multi" (2 TUs) -# exec/<prefix>.xfail or <case-name>.xfail marks a case as expected-fail. -# -# The case name is the longest common prefix of the group's basenames -# with any trailing '_' stripped — so the multi-TU files share a stem -# (e.g. 07_multi_) while each ends in a unique disambiguator. -# -# Per case: -# clang -c <each .c> -> case_<n>.o -# clang -fuse-ld=lld -static -nostdlib *.o -> golden.exe -# cfree ld -o cfree.exe *.o -# <runner> golden.exe / cfree.exe; diff stdout/stderr/exit. - -printf 'Layer D — exe behavioral comparison\n' -shopt -s nullglob -exec_srcs=( "$TEST_DIR"/exec/[0-9]*.c ) -CFREE_BIN="${CFREE:-$ROOT/build/cfree}" -if [ ${#exec_srcs[@]} -eq 0 ]; then - printf ' (no exec cases yet)\n' -else - # Build the unique sorted list of numeric prefixes. - declare -A seen_prefix=() - prefixes=() - for src in "${exec_srcs[@]}"; do - bn="$(basename "$src" .c)" - prefix="${bn%%[!0-9]*}" # leading digits - if [ -z "$prefix" ]; then continue; fi - if [ -z "${seen_prefix[$prefix]:-}" ]; then - seen_prefix[$prefix]=1 - prefixes+=("$prefix") - fi - done - IFS=$'\n' prefixes=( $(printf '%s\n' "${prefixes[@]}" | sort) ); unset IFS - - for prefix in "${prefixes[@]}"; do - # Gather sorted group members. - group=( "$TEST_DIR"/exec/${prefix}_*.c "$TEST_DIR"/exec/${prefix}.c ) - # Filter non-existent (the second pattern usually doesn't match). - real_group=() - for f in "${group[@]}"; do [ -f "$f" ] && real_group+=("$f"); done - if [ ${#real_group[@]} -eq 0 ]; then continue; fi - IFS=$'\n' real_group=( $(printf '%s\n' "${real_group[@]}" | sort) ); unset IFS - - # Case name = longest common prefix of basenames, trimmed of trailing '_'. - first_bn="$(basename "${real_group[0]}" .c)" - case_name="$first_bn" - for f in "${real_group[@]:1}"; do - bn="$(basename "$f" .c)" - new="" - n=0 - while [ $n -lt ${#case_name} ] && [ $n -lt ${#bn} ] \ - && [ "${case_name:$n:1}" = "${bn:$n:1}" ]; do - new+="${case_name:$n:1}" - n=$((n+1)) - done - case_name="$new" - done - case_name="${case_name%_}" - if [ -z "$case_name" ]; then case_name="$prefix"; fi - - name="exec/$case_name" - if [ $have_clang -ne 1 ]; then note_skip "$name" "clang missing"; continue; fi - if [ $have_runner -ne 1 ]; then note_skip "$name" "no aarch64 runner (qemu/podman)"; continue; fi - if [ ! -x "$CFREE_BIN" ]; then note_skip "$name" "cfree binary not built"; continue; fi - - wd="$BUILD_DIR/exec_$case_name" - mkdir -p "$wd" - - # xfail sentinel: <case-name>.xfail (sits beside any one of the .c - # files in the group) marks the whole case expected-fail. - xfail=0 - [ -f "$TEST_DIR/exec/$case_name.xfail" ] && xfail=1 - for f in "${real_group[@]}"; do - [ -f "${f%.c}.xfail" ] && xfail=1 - done - - # Per-case extra compiler flags: <case_name>.cflags beside any group - # member, or named after the shared numeric prefix. - exec_extra_cflags="" - if [ -f "$TEST_DIR/exec/$case_name.cflags" ]; then - exec_extra_cflags="$(cat "$TEST_DIR/exec/$case_name.cflags")" - fi - - # Compile each TU to its own .o. - objs=() - compile_ok=1 - for f in "${real_group[@]}"; do - obj="$wd/$(basename "$f" .c).o" - # shellcheck disable=SC2086 - if ! clang --target=aarch64-linux-gnu -c -O0 -ffreestanding -fno-pic \ - $exec_extra_cflags \ - "$f" -o "$obj" 2> "$wd/clang_c_$(basename "$f" .c).log"; then - compile_ok=0 - break - fi - objs+=("$obj") - done - if [ $compile_ok -ne 1 ]; then - note_skip "$name" "clang -c failed (cross-compile not configured?)" - continue - fi - - if ! clang --target=aarch64-linux-gnu -fuse-ld=lld -static -nostdlib \ - "${objs[@]}" -o "$wd/golden.exe" 2> "$wd/link_golden.log"; then - note_skip "$name" "clang/lld link of golden.exe failed" - continue - fi - - if ! "$CFREE_BIN" ld -o "$wd/cfree.exe" "${objs[@]}" \ - 2> "$wd/link_cfree.log"; then - if [ $xfail -eq 1 ]; then - note_pass "$name (xfail: cfree ld rejected)" - else - note_fail "$name (cfree ld failed)" - sed 's/^/ | /' "$wd/link_cfree.log" - fi - continue - fi - - chmod +x "$wd/cfree.exe" 2>/dev/null || true - - run_aarch64 "$wd/golden.exe" "$wd/golden.out" "$wd/golden.err"; ge=$RUN_RC - run_aarch64 "$wd/cfree.exe" "$wd/cfree.out" "$wd/cfree.err"; ce=$RUN_RC - printf 'rc=%s\n' "$ge" >> "$wd/golden.out" - printf 'rc=%s\n' "$ce" >> "$wd/cfree.out" - - if ! diff -u "$wd/golden.out" "$wd/cfree.out" > "$wd/run.diff" \ - || ! diff -u "$wd/golden.err" "$wd/cfree.err" >> "$wd/run.diff"; then - if [ $xfail -eq 1 ]; then - note_pass "$name (xfail: run output differs)" - else - note_fail "$name (run output differs)" - head -40 "$wd/run.diff" | sed 's/^/ | /' - fi - continue - fi - - if [ $xfail -eq 1 ]; then - note_fail "$name (XPASS — case marked xfail but everything matched; remove .xfail)" - else - note_pass "$name" - fi - done -fi -printf '\n' - # ----- summary ----------------------------------------------------------- printf 'Summary: %s passed, %s failed, %s skipped\n' \ diff --git a/test/link/run.sh b/test/link/run.sh @@ -237,9 +237,9 @@ for case_dir in "$TEST_DIR/cases"/*/; do if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt_${base}.err"; then r_ok=0; break fi - "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" \ + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" filter \ >"$work/${base}_golden.norm" 2>/dev/null - "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" \ + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" filter \ >"$work/${base}_rt.norm" 2>/dev/null if ! diff -u "$work/${base}_golden.norm" \ "$work/${base}_rt.norm" \