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:
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" \