kit

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

commit d56632d51f4d3fd7553ac1de5c04d7a0c0fc35ba
parent 43883bdb6d5cea0bc0f8d4e57ec7a65db1b2d4e9
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 09:02:39 -0700

test/link: add unified linker and JIT behavioral test corpus

30 cases covering all implemented AArch64 reloc kinds, symbol
bindings/visibility, init/fini arrays, COMDAT dedup, GC sections,
archives, extern resolver, and linker error diagnostics.

Each case runs through three independent harness paths:
  R  obj roundtrip (elf_read + elf_emit structural diff)
  E  ELF exec (cfree_link_exe → qemu/podman)
  J  JIT (cfree_link_jit → in-process on aarch64 host)

All cases use a test_main() / test_post_fini() convention with a
freestanding _start stub (start.c) that walks init_array and
fini_array, mirrored by jit_runner which calls cfree_jit_run_dtors.

API: add cfree_jit_run_dtors() declaration + stub to link_jit.c.
test.mk: wire test-link into make test.

Diffstat:
Minclude/cfree.h | 7+++++--
Msrc/link/link_jit.c | 6++++++
Atest/link/CORPUS.md | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/cases/01_exit_value/a.c | 3+++
Atest/link/cases/02_rodata_u8/a.c | 5+++++
Atest/link/cases/03_rodata_u16/a.c | 6++++++
Atest/link/cases/04_rodata_u32/a.c | 6++++++
Atest/link/cases/05_rodata_u64/a.c | 6++++++
Atest/link/cases/06_rodata_u128/a.c | 13+++++++++++++
Atest/link/cases/07_data_rw/a.c | 8++++++++
Atest/link/cases/08_bss_zero/a.c | 5+++++
Atest/link/cases/09_data_fnptr/a.c | 4++++
Atest/link/cases/10_call_cross_tu/a.c | 3+++
Atest/link/cases/10_call_cross_tu/b.c | 1+
Atest/link/cases/11_data_cross_tu/a.c | 6++++++
Atest/link/cases/11_data_cross_tu/b.c | 2++
Atest/link/cases/12_ptr_cross_tu/a.c | 4++++
Atest/link/cases/12_ptr_cross_tu/b.c | 1+
Atest/link/cases/13_hidden_call/a.c | 3+++
Atest/link/cases/13_hidden_call/b.c | 2++
Atest/link/cases/14_weak_present/a.c | 5+++++
Atest/link/cases/14_weak_present/b.c | 1+
Atest/link/cases/15_weak_override/a.c | 6++++++
Atest/link/cases/15_weak_override/b.c | 1+
Atest/link/cases/16_weak_undef/a.c | 8++++++++
Atest/link/cases/17_common_coalesce/a.c | 8++++++++
Atest/link/cases/17_common_coalesce/b.c | 2++
Atest/link/cases/18_static_local/a.c | 3+++
Atest/link/cases/18_static_local/b.c | 2++
Atest/link/cases/19_three_tu/a.c | 3+++
Atest/link/cases/19_three_tu/b.c | 2++
Atest/link/cases/19_three_tu/c.c | 1+
Atest/link/cases/20_init_array/a.c | 6++++++
Atest/link/cases/21_fini_array/a.c | 8++++++++
Atest/link/cases/22_init_fini_both/a.c | 10++++++++++
Atest/link/cases/23_init_order/a.c | 15+++++++++++++++
Atest/link/cases/23_init_order/b.c | 2++
Atest/link/cases/24_comdat_dedup/a.c | 8++++++++
Atest/link/cases/24_comdat_dedup/b.c | 1+
Atest/link/cases/25_gc_sections/a.c | 8++++++++
Atest/link/cases/25_gc_sections/gc_absent | 1+
Atest/link/cases/25_gc_sections/linker_flags | 1+
Atest/link/cases/26_archive_demand/a.c | 5+++++
Atest/link/cases/26_archive_demand/archive_b | 1+
Atest/link/cases/26_archive_demand/b.c | 1+
Atest/link/cases/27_archive_whole/a.c | 12++++++++++++
Atest/link/cases/27_archive_whole/archive_b | 1+
Atest/link/cases/27_archive_whole/b.c | 3+++
Atest/link/cases/28_extern_resolver/a.c | 7+++++++
Atest/link/cases/28_extern_resolver/jit_only | 1+
Atest/link/cases/28_extern_resolver/use_resolver | 1+
Atest/link/cases/29_jit_lookup_miss/a.c | 4++++
Atest/link/cases/29_jit_lookup_miss/jit_only | 1+
Atest/link/cases/30_undef_strong/a.c | 5+++++
Atest/link/cases/30_undef_strong/link_fail | 1+
Atest/link/harness/jit_runner.c | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/harness/link_exe_runner.c | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/harness/start.c | 46++++++++++++++++++++++++++++++++++++++++++++++
Atest/link/run.sh | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 10++++++++--
60 files changed, 1123 insertions(+), 4 deletions(-)

diff --git a/include/cfree.h b/include/cfree.h @@ -256,8 +256,11 @@ const uint8_t* cfree_writer_mem_bytes(CfreeWriter*, size_t* len_out); * libcfree); dlsym-shaped — the caller casts to whatever function * signature the JITed symbol actually has (e.g. int(*)(int, char**) for * `main`). Returns NULL on miss. */ -void cfree_jit_free (CfreeJit*); -void* cfree_jit_lookup(CfreeJit*, const char* name); +void cfree_jit_free (CfreeJit*); +void* cfree_jit_lookup (CfreeJit*, const char* name); +/* Run all fini_array destructors in reverse order. Call after the last + * use of JITed code, before cfree_jit_free. */ +void cfree_jit_run_dtors (CfreeJit*); /* ----- JIT image inspection ----- * diff --git a/src/link/link_jit.c b/src/link/link_jit.c @@ -201,3 +201,9 @@ void cfree_jit_sym_iter_free(CfreeJitSymIter* it) { (void)it; } + +void cfree_jit_run_dtors(CfreeJit* jit) +{ + (void)jit; + /* TODO: walk fini_array sections in reverse and call each fn pointer. */ +} diff --git a/test/link/CORPUS.md b/test/link/CORPUS.md @@ -0,0 +1,146 @@ +# test/link — Linker and JIT test corpus + +Unified behavioral test suite for cfree's linker (ELF executable emission) +and JIT (in-process execution). Each case is a directory of C source files +with a shared `test_main()` convention, run through three independent paths. + +## Paths + +| Path | What it tests | Requires | +|------|--------------|----------| +| **R** roundtrip | `elf_read` + `elf_emit` fidelity; structural diff via `llvm-readelf` | clang cross, cfree-roundtrip, llvm-readelf, python3 | +| **E** exec | `cfree_link_exe` layout + reloc application; runs via qemu/podman | clang cross, link-exe-runner, qemu or podman | +| **J** JIT | `cfree_link_jit` + `cfree_jit_*`; runs natively on aarch64 host | clang cross, jit-runner (aarch64 only) | + +## Convention + +Every case exposes: + +```c +int test_main(void); // primary test; 0 = pass +int test_post_fini(void); // optional post-dtor check (weak default: 0) +``` + +The `_start` stub (harness/start.c) runs `init_array` ctors, calls +`test_main`, runs `fini_array` dtors, calls `test_post_fini`, and exits. +`jit_runner` mirrors this with `cfree_jit_run_dtors` + `test_post_fini` +lookup. `expected` (default 0) is the exit code / return value the harness +expects from the combined sequence. + +## Case markers + +| File | Meaning | +|------|---------| +| `expected` | expected return value (default 0) | +| `jit_only` | skip R and E; run J only | +| `link_fail` | E and J paths expect the linker to fail cleanly (non-zero, no signal) | +| `use_resolver` | pass `--use-resolver` to jit_runner | +| `linker_flags` | one flag per line passed to link-exe-runner and jit-runner | +| `gc_absent` | one symbol per line that must be absent post --gc-sections | +| `archive_b` | package b.o as b.a; content `demand` (normal) or `whole` (--whole-archive) | + +## Case index + +### Group A — single-TU, reloc coverage + +| # | Name | Reloc(s) | C construct | +|---|------|----------|-------------| +| 01 | `exit_value` | CALL26 intra-TU | static helper call | +| 02 | `rodata_u8` | LDST8 | `const char` load | +| 03 | `rodata_u16` | LDST16 | `const uint16_t` load | +| 04 | `rodata_u32` | LDST32 | `const uint32_t` load | +| 05 | `rodata_u64` | LDST64 | `const uint64_t` load | +| 06 | `rodata_u128` | LDST128 | `const __uint128_t` load | +| 07 | `data_rw` | ADD_ABS_LO12_NC + LDST64 store | address-of + write + read | +| 08 | `bss_zero` | NOBITS | uninitialized static reads 0 | +| 09 | `data_fnptr` | ABS64 | function pointer in .rodata | + +Cases 02–09 all pair ADR_PREL_PG_HI21 with their primary LDST reloc. + +### Group B — multi-TU, symbol resolution + +| # | Name | Exercises | +|---|------|-----------| +| 10 | `call_cross_tu` | CALL26 cross-TU; STB_GLOBAL | +| 11 | `data_cross_tu` | LDST64 cross-TU | +| 12 | `ptr_cross_tu` | ABS64 cross-TU function pointer | +| 13 | `hidden_call` | CALL26 + STV_HIDDEN | +| 14 | `weak_present` | STB_WEAK defined; weak variable read | +| 15 | `weak_override` | strong definition beats weak | +| 16 | `weak_undef` | STB_WEAK + SHN_UNDEF; NULL guard | +| 17 | `common_coalesce` | SHN_COMMON; tentative defs merge | +| 18 | `static_local` | STB_LOCAL; static helper not exported | +| 19 | `three_tu` | CALL26 chain across three inputs | + +### Group C — constructors and destructors + +| # | Name | Exercises | +|---|------|-----------| +| 20 | `init_array` | SHT_INIT_ARRAY ctor sets flag before test_main | +| 21 | `fini_array` | SHT_FINI_ARRAY dtor; checked via test_post_fini | +| 22 | `init_fini_both` | ctor + dtor in same TU | +| 23 | `init_order` | priority-ordered ctors across two TUs (101 before 102) | + +### Group D — COMDAT, GC, archives + +| # | Name | Exercises | +|---|------|-----------| +| 24 | `comdat_dedup` | STB_WEAK dedup; multiply-defined weak fn → one copy | +| 25 | `gc_sections` | `--gc-sections`; unreachable_fn removed; JIT lookup returns NULL | +| 26 | `archive_demand` | b.o in b.a; demand-loaded to satisfy reference | +| 27 | `archive_whole` | b.a with `--whole-archive`; unreferenced ctor runs | + +### Group E — JIT-specific + +| # | Name | Exercises | +|---|------|-----------| +| 28 | `extern_resolver` | `CfreeExternResolver` provides address for undefined symbol | +| 29 | `jit_lookup_miss` | `cfree_jit_lookup` returns NULL for unknown name | + +### Group F — error cases + +| # | Name | Exercises | +|---|------|-----------| +| 30 | `undef_strong` | unresolved strong symbol → linker error, clean non-zero exit | + +## Reloc coverage matrix + +| Reloc | Covered by | +|-------|-----------| +| `R_AARCH64_CALL26` intra-TU | 01 | +| `R_AARCH64_CALL26` cross-TU | 10, 12–15, 18, 19 | +| `R_AARCH64_ADR_PREL_PG_HI21` | 02–09, 11, 17 (paired with each LDST) | +| `R_AARCH64_ADD_ABS_LO12_NC` | 07, 12 | +| `R_AARCH64_LDST8_ABS_LO12_NC` | 02 | +| `R_AARCH64_LDST16_ABS_LO12_NC` | 03 | +| `R_AARCH64_LDST32_ABS_LO12_NC` | 04 | +| `R_AARCH64_LDST64_ABS_LO12_NC` | 05, 07, 11, 17 | +| `R_AARCH64_LDST128_ABS_LO12_NC` | 06 | +| `R_ABS64` | 09, 12 | +| `R_AARCH64_PREL32` (from `.eh_frame`) | implicit in 01–19 (Path R) | + +Skipped (not reliably generated from C on AArch64): +- `R_ABS32` — 32-bit absolute pointer; compilers use 64-bit on AArch64 +- `R_REL64` / `R_PC64` — not emitted by clang in standard C mode +- `R_GOT32`, `R_PLT32` — shared-library path; deferred milestone + +## Impl requirements + +Features required by the corpus but not yet implemented (tests will fail +until the impl lands): + +1. `cfree_jit_from_image` must execute `init_array` ctors after mprotect + (needed by cases 20–23, 27). +2. `cfree_jit_run_dtors` must walk `fini_array` sections in reverse + (needed by cases 21–22). +3. `cfree-ld` / `link_emit_image_writer` must define linker-synthesized + boundary symbols `__init_array_start/end`, `__fini_array_start/end` + (needed by start.c, cases 20–23, 27). +4. `cfree-ld` must sort `.init_array.NNN` sections by priority suffix + (needed by case 23). +5. `cfree_link_exe` / `cfree_link_jit` must support `CfreeLinkOptions.gc_sections` + (needed by case 25). +6. Archive reader (`link_add_archive_bytes`) must support demand-loading + and `whole_archive` (needed by cases 26–27). +7. `cfree_link_jit` must return non-zero on unresolved strong symbols + without `extern_resolver` (needed by case 30). diff --git a/test/link/cases/01_exit_value/a.c b/test/link/cases/01_exit_value/a.c @@ -0,0 +1,3 @@ +/* CALL26 intra-TU: test_main calls a static helper. */ +static int helper(int x) { return x - x; } +int test_main(void) { return helper(42); } diff --git a/test/link/cases/02_rodata_u8/a.c b/test/link/cases/02_rodata_u8/a.c @@ -0,0 +1,5 @@ +/* ADR_PREL_PG_HI21 + LDST8_ABS_LO12_NC: load a const char. */ +static const char g = 42; +int test_main(void) { + return *(volatile const char*)&g == 42 ? 0 : 1; +} diff --git a/test/link/cases/03_rodata_u16/a.c b/test/link/cases/03_rodata_u16/a.c @@ -0,0 +1,6 @@ +/* ADR_PREL_PG_HI21 + LDST16_ABS_LO12_NC: load a const uint16_t. */ +#include <stdint.h> +static const uint16_t g = 0x1234u; +int test_main(void) { + return *(volatile const uint16_t*)&g == 0x1234u ? 0 : 1; +} diff --git a/test/link/cases/04_rodata_u32/a.c b/test/link/cases/04_rodata_u32/a.c @@ -0,0 +1,6 @@ +/* ADR_PREL_PG_HI21 + LDST32_ABS_LO12_NC: load a const uint32_t. */ +#include <stdint.h> +static const uint32_t g = 0xdeadbeefU; +int test_main(void) { + return *(volatile const uint32_t*)&g == 0xdeadbeefU ? 0 : 1; +} diff --git a/test/link/cases/05_rodata_u64/a.c b/test/link/cases/05_rodata_u64/a.c @@ -0,0 +1,6 @@ +/* ADR_PREL_PG_HI21 + LDST64_ABS_LO12_NC: load a const uint64_t. */ +#include <stdint.h> +static const uint64_t g = UINT64_C(0x0102030405060708); +int test_main(void) { + return *(volatile const uint64_t*)&g == UINT64_C(0x0102030405060708) ? 0 : 1; +} diff --git a/test/link/cases/06_rodata_u128/a.c b/test/link/cases/06_rodata_u128/a.c @@ -0,0 +1,13 @@ +/* ADR_PREL_PG_HI21 + LDST128_ABS_LO12_NC: load a const __uint128_t. */ +#include <stdint.h> +typedef unsigned __int128 u128; +static const u128 g = + ((u128)UINT64_C(0x0102030405060708) << 64) | UINT64_C(0x090a0b0c0d0e0f10); + +int test_main(void) { + volatile u128 v = g; + uint64_t hi = (uint64_t)(v >> 64); + uint64_t lo = (uint64_t)(v); + return (hi == UINT64_C(0x0102030405060708) && + lo == UINT64_C(0x090a0b0c0d0e0f10)) ? 0 : 1; +} diff --git a/test/link/cases/07_data_rw/a.c b/test/link/cases/07_data_rw/a.c @@ -0,0 +1,8 @@ +/* ADR_PREL_PG_HI21 + ADD_ABS_LO12_NC (address-of) + LDST64 store+load. */ +#include <stdint.h> +static uint64_t g; +int test_main(void) { + uint64_t* p = &g; /* ADD_ABS_LO12_NC */ + *p = UINT64_C(0xdeadbeefcafebabe); /* LDST64 store */ + return (*p == UINT64_C(0xdeadbeefcafebabe)) ? 0 : 1; +} diff --git a/test/link/cases/08_bss_zero/a.c b/test/link/cases/08_bss_zero/a.c @@ -0,0 +1,5 @@ +/* NOBITS section: a static variable with no initializer must read as 0. */ +static int g; +int test_main(void) { + return *(volatile int*)&g == 0 ? 0 : 1; +} diff --git a/test/link/cases/09_data_fnptr/a.c b/test/link/cases/09_data_fnptr/a.c @@ -0,0 +1,4 @@ +/* ABS64: a function pointer stored in .rodata, called through. */ +static int target(void) { return 0; } +static int (* const fp)(void) = target; +int test_main(void) { return fp(); } diff --git a/test/link/cases/10_call_cross_tu/a.c b/test/link/cases/10_call_cross_tu/a.c @@ -0,0 +1,3 @@ +/* CALL26 cross-TU: a.c calls add() defined in b.c. */ +int add(int a, int b); +int test_main(void) { return add(3, -3); } diff --git a/test/link/cases/10_call_cross_tu/b.c b/test/link/cases/10_call_cross_tu/b.c @@ -0,0 +1 @@ +int add(int a, int b) { return a + b; } diff --git a/test/link/cases/11_data_cross_tu/a.c b/test/link/cases/11_data_cross_tu/a.c @@ -0,0 +1,6 @@ +/* ADR_PREL_PG_HI21 + LDST64 cross-TU: read a const uint64_t from b.c. */ +#include <stdint.h> +extern const uint64_t g_val; +int test_main(void) { + return g_val == UINT64_C(0xdeadbeefcafebabe) ? 0 : 1; +} diff --git a/test/link/cases/11_data_cross_tu/b.c b/test/link/cases/11_data_cross_tu/b.c @@ -0,0 +1,2 @@ +#include <stdint.h> +const uint64_t g_val = UINT64_C(0xdeadbeefcafebabe); diff --git a/test/link/cases/12_ptr_cross_tu/a.c b/test/link/cases/12_ptr_cross_tu/a.c @@ -0,0 +1,4 @@ +/* ABS64 cross-TU: function pointer to b.c's target_fn stored in .rodata. */ +int target_fn(void); +static int (* const fp)(void) = target_fn; +int test_main(void) { return fp(); } diff --git a/test/link/cases/12_ptr_cross_tu/b.c b/test/link/cases/12_ptr_cross_tu/b.c @@ -0,0 +1 @@ +int target_fn(void) { return 0; } diff --git a/test/link/cases/13_hidden_call/a.c b/test/link/cases/13_hidden_call/a.c @@ -0,0 +1,3 @@ +/* CALL26 + STV_HIDDEN: call a hidden-visibility function from b.c. */ +int hidden_fn(void); +int test_main(void) { return hidden_fn(); } diff --git a/test/link/cases/13_hidden_call/b.c b/test/link/cases/13_hidden_call/b.c @@ -0,0 +1,2 @@ +__attribute__((visibility("hidden"))) +int hidden_fn(void) { return 0; } diff --git a/test/link/cases/14_weak_present/a.c b/test/link/cases/14_weak_present/a.c @@ -0,0 +1,5 @@ +/* STB_WEAK defined: read a weak variable from b.c; it is present. */ +extern __attribute__((weak)) int weak_val; +int test_main(void) { + return weak_val == 7 ? 0 : 1; +} diff --git a/test/link/cases/14_weak_present/b.c b/test/link/cases/14_weak_present/b.c @@ -0,0 +1 @@ +__attribute__((weak)) int weak_val = 7; diff --git a/test/link/cases/15_weak_override/a.c b/test/link/cases/15_weak_override/a.c @@ -0,0 +1,6 @@ +/* Strong def beats weak def: a.c has int val=99 (strong), b.c has weak val=7. + * Linked value must be 99. */ +int val = 99; +int test_main(void) { + return val == 99 ? 0 : 1; +} diff --git a/test/link/cases/15_weak_override/b.c b/test/link/cases/15_weak_override/b.c @@ -0,0 +1 @@ +__attribute__((weak)) int val = 7; diff --git a/test/link/cases/16_weak_undef/a.c b/test/link/cases/16_weak_undef/a.c @@ -0,0 +1,8 @@ +/* STB_WEAK + SHN_UNDEF: weak extern function declared but never defined. + * Its address resolves to NULL; test guards before calling. */ +extern __attribute__((weak)) int weak_fn(void); +int test_main(void) { + if ((void*)weak_fn != (void*)0) + return 1; + return 0; +} diff --git a/test/link/cases/17_common_coalesce/a.c b/test/link/cases/17_common_coalesce/a.c @@ -0,0 +1,8 @@ +/* SHN_COMMON: tentative definition in both TUs coalesces into one copy. + * b.c writes to shared_val; a.c reads it back through the same object. */ +int shared_val; +void set_shared(int v); +int test_main(void) { + set_shared(99); + return shared_val == 99 ? 0 : 1; +} diff --git a/test/link/cases/17_common_coalesce/b.c b/test/link/cases/17_common_coalesce/b.c @@ -0,0 +1,2 @@ +int shared_val; +void set_shared(int v) { shared_val = v; } diff --git a/test/link/cases/18_static_local/a.c b/test/link/cases/18_static_local/a.c @@ -0,0 +1,3 @@ +/* STB_LOCAL: static helper in b.c is not exported; a.c calls a wrapper. */ +int wrapper(void); +int test_main(void) { return wrapper(); } diff --git a/test/link/cases/18_static_local/b.c b/test/link/cases/18_static_local/b.c @@ -0,0 +1,2 @@ +static int local_helper(void) { return 0; } +int wrapper(void) { return local_helper(); } diff --git a/test/link/cases/19_three_tu/a.c b/test/link/cases/19_three_tu/a.c @@ -0,0 +1,3 @@ +/* Three-TU CALL26 chain: a → b → c. */ +int b_fn(void); +int test_main(void) { return b_fn(); } diff --git a/test/link/cases/19_three_tu/b.c b/test/link/cases/19_three_tu/b.c @@ -0,0 +1,2 @@ +int c_fn(void); +int b_fn(void) { return c_fn(); } diff --git a/test/link/cases/19_three_tu/c.c b/test/link/cases/19_three_tu/c.c @@ -0,0 +1 @@ +int c_fn(void) { return 0; } diff --git a/test/link/cases/20_init_array/a.c b/test/link/cases/20_init_array/a.c @@ -0,0 +1,6 @@ +/* SHT_INIT_ARRAY: constructor runs before test_main and sets g_ready. */ +static int g_ready = 0; + +static void __attribute__((constructor)) init(void) { g_ready = 1; } + +int test_main(void) { return g_ready == 1 ? 0 : 1; } diff --git a/test/link/cases/21_fini_array/a.c b/test/link/cases/21_fini_array/a.c @@ -0,0 +1,8 @@ +/* SHT_FINI_ARRAY: destructor sets g_dtor_ran; test_post_fini checks it. + * test_main returns 0 immediately; the harness calls dtors then test_post_fini. */ +static int g_dtor_ran = 0; + +static void __attribute__((destructor)) fini(void) { g_dtor_ran = 1; } + +int test_main(void) { return 0; } +int test_post_fini(void) { return g_dtor_ran == 1 ? 0 : 1; } diff --git a/test/link/cases/22_init_fini_both/a.c b/test/link/cases/22_init_fini_both/a.c @@ -0,0 +1,10 @@ +/* INIT_ARRAY + FINI_ARRAY: ctor sets g_ctor_ran; test_main checks it. + * Dtor sets g_dtor_ran; test_post_fini checks it. */ +static int g_ctor_ran = 0; +static int g_dtor_ran = 0; + +static void __attribute__((constructor)) init(void) { g_ctor_ran = 1; } +static void __attribute__((destructor)) fini(void) { g_dtor_ran = 1; } + +int test_main(void) { return g_ctor_ran == 1 ? 0 : 1; } +int test_post_fini(void) { return g_dtor_ran == 1 ? 0 : 1; } diff --git a/test/link/cases/23_init_order/a.c b/test/link/cases/23_init_order/a.c @@ -0,0 +1,15 @@ +/* INIT_ARRAY priority ordering: lower priority number runs first. + * a.c registers priority 101 (runs first), b.c registers 102 (runs second). + * test_main verifies the sequence buffer matches [1, 2]. */ +static int g_seq[2]; +static int g_pos = 0; + +void record_seq(int v) { + if (g_pos < 2) g_seq[g_pos++] = v; +} + +static void __attribute__((constructor(101))) init_a(void) { record_seq(1); } + +int test_main(void) { + return (g_seq[0] == 1 && g_seq[1] == 2) ? 0 : 1; +} diff --git a/test/link/cases/23_init_order/b.c b/test/link/cases/23_init_order/b.c @@ -0,0 +1,2 @@ +void record_seq(int v); +static void __attribute__((constructor(102))) init_b(void) { record_seq(2); } diff --git a/test/link/cases/24_comdat_dedup/a.c b/test/link/cases/24_comdat_dedup/a.c @@ -0,0 +1,8 @@ +/* Deduplication: comdat_fn is defined in both TUs as STB_WEAK. + * The linker must keep exactly one copy; test_main verifies the result. + * Note: true SHT_GROUP / COMDAT dedup is verified separately via + * test/elf/cases/13_comdat.c roundtrip; this case tests the linker's + * weak-merge path end-to-end through exec and JIT. */ +__attribute__((noinline, weak)) int comdat_fn(void) { return 42; } + +int test_main(void) { return comdat_fn() == 42 ? 0 : 1; } diff --git a/test/link/cases/24_comdat_dedup/b.c b/test/link/cases/24_comdat_dedup/b.c @@ -0,0 +1 @@ +__attribute__((noinline, weak)) int comdat_fn(void) { return 42; } diff --git a/test/link/cases/25_gc_sections/a.c b/test/link/cases/25_gc_sections/a.c @@ -0,0 +1,8 @@ +/* --gc-sections: unreachable_fn is not reachable from test_main. + * With --gc-sections it is removed from the output; without it remains. + * test_main passes unconditionally; the JIT path additionally verifies + * that cfree_jit_lookup("unreachable_fn") returns NULL after GC. */ +__attribute__((noinline)) int live_fn(void) { return 0; } +__attribute__((noinline)) int unreachable_fn(void) { return 1; } + +int test_main(void) { return live_fn(); } diff --git a/test/link/cases/25_gc_sections/gc_absent b/test/link/cases/25_gc_sections/gc_absent @@ -0,0 +1 @@ +unreachable_fn diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25_gc_sections/linker_flags @@ -0,0 +1 @@ +--gc-sections diff --git a/test/link/cases/26_archive_demand/a.c b/test/link/cases/26_archive_demand/a.c @@ -0,0 +1,5 @@ +/* Archive demand-loading: fn_from_archive is defined in b.c, which is + * packaged as b.a. The linker must pull b.o from the archive to satisfy + * the reference from a.c. */ +int fn_from_archive(void); +int test_main(void) { return fn_from_archive(); } diff --git a/test/link/cases/26_archive_demand/archive_b b/test/link/cases/26_archive_demand/archive_b @@ -0,0 +1 @@ +demand diff --git a/test/link/cases/26_archive_demand/b.c b/test/link/cases/26_archive_demand/b.c @@ -0,0 +1 @@ +int fn_from_archive(void) { return 0; } diff --git a/test/link/cases/27_archive_whole/a.c b/test/link/cases/27_archive_whole/a.c @@ -0,0 +1,12 @@ +/* --whole-archive: b.c is packaged as b.a and has no symbol referenced by a.c. + * With --whole-archive, b.o is included unconditionally so its constructor + * runs and b_get_flag() becomes available via a weak reference. + * Without --whole-archive, the weak reference resolves to NULL and the + * test returns 1 (fail). */ +extern __attribute__((weak)) int b_get_flag(void); + +int test_main(void) { + if ((void*)b_get_flag == (void*)0) + return 1; + return b_get_flag() == 1 ? 0 : 1; +} diff --git a/test/link/cases/27_archive_whole/archive_b b/test/link/cases/27_archive_whole/archive_b @@ -0,0 +1 @@ +whole diff --git a/test/link/cases/27_archive_whole/b.c b/test/link/cases/27_archive_whole/b.c @@ -0,0 +1,3 @@ +static int g_flag = 0; +static void __attribute__((constructor)) b_init(void) { g_flag = 1; } +int b_get_flag(void) { return g_flag; } diff --git a/test/link/cases/28_extern_resolver/a.c b/test/link/cases/28_extern_resolver/a.c @@ -0,0 +1,7 @@ +/* JIT extern resolver: external_value is not defined in any .o. + * The jit_runner provides a resolver that maps every unresolved symbol + * to a static int == 42. test_main reads external_value and checks it. */ +extern int external_value; +int test_main(void) { + return external_value == 42 ? 0 : 1; +} diff --git a/test/link/cases/28_extern_resolver/jit_only b/test/link/cases/28_extern_resolver/jit_only @@ -0,0 +1 @@ +1 diff --git a/test/link/cases/28_extern_resolver/use_resolver b/test/link/cases/28_extern_resolver/use_resolver @@ -0,0 +1 @@ +1 diff --git a/test/link/cases/29_jit_lookup_miss/a.c b/test/link/cases/29_jit_lookup_miss/a.c @@ -0,0 +1,4 @@ +/* JIT lookup miss: test_main returns 0; the jit_runner always verifies + * that cfree_jit_lookup(jit, "__cfree_test_sentinel__") returns NULL, + * exercising the lookup-miss path on every JIT run. */ +int test_main(void) { return 0; } diff --git a/test/link/cases/29_jit_lookup_miss/jit_only b/test/link/cases/29_jit_lookup_miss/jit_only @@ -0,0 +1 @@ +1 diff --git a/test/link/cases/30_undef_strong/a.c b/test/link/cases/30_undef_strong/a.c @@ -0,0 +1,5 @@ +/* Linker error: missing_fn is declared but never defined. + * The linker must reject this with a diagnostic and non-zero exit. + * The harness verifies: non-zero exit AND exit code < 128 (no signal). */ +int missing_fn(void); +int test_main(void) { return missing_fn(); } diff --git a/test/link/cases/30_undef_strong/link_fail b/test/link/cases/30_undef_strong/link_fail @@ -0,0 +1 @@ +1 diff --git a/test/link/harness/jit_runner.c b/test/link/harness/jit_runner.c @@ -0,0 +1,184 @@ +/* jit_runner — Path J harness driver. + * + * Usage: + * jit_runner [--gc-sections] [--use-resolver] + * [--archive [--whole-archive] <lib.a>] <in.o> ... + * + * Reads .o (and optionally .a) inputs, calls cfree_link_jit (which runs + * init_array ctors), calls test_main(), calls cfree_jit_run_dtors(), then + * calls test_post_fini() if it exists. Exits with the combined result. + * + * --use-resolver: enables a default extern resolver that returns the + * address of a static int == 42 for any unresolved symbol. Used for + * the extern_resolver test case (28). + * + * Sanity: after every successful JIT load, verifies that + * cfree_jit_lookup(jit, "__cfree_test_sentinel__") returns NULL (tests + * the lookup-miss path, covering case 29). + * + * Runs only on aarch64 hosts. Compiled against libcfree.a with + * -I$(ROOT)/include. */ + +#include <cfree.h> + +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +static void* h_alloc (CfreeHeap* h, size_t n, size_t a) { (void)h;(void)a; return malloc(n); } +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) +{ (void)h;(void)o;(void)a; return realloc(p,n); } +static void h_free (CfreeHeap* h, void* p, size_t n) { (void)h;(void)n; free(p); } +static CfreeHeap g_heap = { h_alloc, h_realloc, h_free, NULL }; + +static void diag_fn(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) +{ + static const char* names[] = { "note","warning","error","fatal" }; + (void)s;(void)loc; + fprintf(stderr, "%s: ", names[k]); vfprintf(stderr, fmt, ap); fputc('\n', stderr); +} +static CfreeDiagSink g_diag = { diag_fn, NULL, 0, 0 }; + +static int slurp(const char* path, uint8_t** out, size_t* len) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + struct stat sb; + if (fstat(fd, &sb) < 0) { close(fd); return -1; } + size_t n = (size_t)sb.st_size; + uint8_t* buf = malloc(n ? n : 1); + if (!buf) { close(fd); return -1; } + size_t got = 0; + while (got < n) { + ssize_t k = read(fd, buf + got, n - got); + if (k <= 0) { free(buf); close(fd); return -1; } + got += (size_t)k; + } + close(fd); *out = buf; *len = n; return 0; +} + +/* Default extern resolver: returns address of a static int == 42. + * Used by case 28 (extern_resolver); harmless for any case without + * unresolved symbols since the resolver is never invoked. */ +static int g_extern_default_value = 42; +static void* extern_resolver(void* user, const char* name) +{ + (void)user; (void)name; + return &g_extern_default_value; +} + +int main(int argc, char** argv) +{ + int gc_sections = 0; + int use_resolver = 0; + int next_archive = 0; + int next_whole = 0; + /* --check-absent SYM: after link, verify symbol is not in image. */ + const char* check_absent = NULL; + + CfreeBytesInput objs[64]; + CfreeBytesInputArchive archives[16]; + uint32_t nobj = 0, narc = 0; + uint8_t* bufs[80]; + int nbufs = 0; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--gc-sections")) { gc_sections = 1; } + else if (!strcmp(argv[i], "--use-resolver")) { use_resolver = 1; } + else if (!strcmp(argv[i], "--archive")) { next_archive = 1; } + else if (!strcmp(argv[i], "--whole-archive")) { next_whole = 1; next_archive = 1; } + else if (!strcmp(argv[i], "--check-absent") && i+1 < argc) { check_absent = argv[++i]; } + else { + uint8_t* data; size_t len; + if (slurp(argv[i], &data, &len)) { + fprintf(stderr, "jit-runner: cannot read %s\n", argv[i]); + return 2; + } + bufs[nbufs++] = data; + if (next_archive) { + CfreeBytesInputArchive* a = &archives[narc++]; + memset(a, 0, sizeof(*a)); + a->input.name = argv[i]; a->input.data = data; a->input.len = len; + a->whole_archive = (uint8_t)next_whole; + next_archive = 0; next_whole = 0; + } else { + CfreeBytesInput* o = &objs[nobj++]; + memset(o, 0, sizeof(*o)); + o->name = argv[i]; o->data = data; o->len = len; + } + } + } + + CfreeTarget target; + memset(&target, 0, sizeof(target)); + target.arch = CFREE_ARCH_ARM_64; + target.os = CFREE_OS_LINUX; + target.obj = CFREE_OBJ_ELF; + + CfreeEnv env; + memset(&env, 0, sizeof(env)); + env.heap = &g_heap; + env.diag = &g_diag; + + CfreeCompiler* c = cfree_compiler_new(target, &env); + if (!c) { fprintf(stderr, "jit-runner: compiler_new failed\n"); return 2; } + + CfreeLinkOptions opts; + memset(&opts, 0, sizeof(opts)); + opts.inputs.obj_bytes = nobj ? objs : NULL; opts.inputs.nobj_bytes = nobj; + opts.inputs.archives = narc ? archives : NULL; opts.inputs.narchives = narc; + opts.inputs.entry = "test_main"; + opts.gc_sections = gc_sections; + if (use_resolver) { + opts.inputs.extern_resolver = extern_resolver; + opts.inputs.extern_resolver_user = NULL; + } + + CfreeJit* jit = NULL; + int rc = cfree_link_jit(c, &opts, &jit); + for (int i = 0; i < nbufs; i++) free(bufs[i]); + if (rc || !jit) { + cfree_compiler_free(c); + return 1; + } + + /* Sanity: a never-defined sentinel must not be found (covers case 29). */ + if (cfree_jit_lookup(jit, "__cfree_test_sentinel__")) { + fprintf(stderr, "jit-runner: sentinel lookup unexpectedly non-NULL\n"); + cfree_jit_run_dtors(jit); + cfree_jit_free(jit); + cfree_compiler_free(c); + return 1; + } + + /* --check-absent SYM: verify symbol was removed (e.g. by --gc-sections). */ + if (check_absent) { + int absent_ok = cfree_jit_lookup(jit, check_absent) == NULL; + if (!absent_ok) + fprintf(stderr, "jit-runner: symbol '%s' present but expected absent\n", + check_absent); + cfree_jit_run_dtors(jit); + cfree_jit_free(jit); + cfree_compiler_free(c); + return absent_ok ? 0 : 1; + } + + int (*fn)(void) = cfree_jit_lookup(jit, "test_main"); + int result = fn ? fn() : 1; + + cfree_jit_run_dtors(jit); + + if (result == 0) { + int (*post)(void) = cfree_jit_lookup(jit, "test_post_fini"); + if (post) result = post(); + } + + cfree_jit_free(jit); + cfree_compiler_free(c); + return result; +} diff --git a/test/link/harness/link_exe_runner.c b/test/link/harness/link_exe_runner.c @@ -0,0 +1,153 @@ +/* link_exe_runner — Path E harness driver. + * + * Usage: + * link_exe_runner [--gc-sections] [--entry NAME] -o <out.exe> + * [--archive [--whole-archive] <lib.a>] <in.o> ... + * + * Reads inputs, calls cfree_link_exe, writes the ELF executable. + * Exit 0 on success, 1 on link error, 2 on I/O/setup error. + * compiler_panic exits the process directly (via exit/abort) — the + * harness treats any non-zero exit as a link failure for link_fail cases. + * + * Compiled against libcfree.a with -I$(ROOT)/include. */ + +#include <cfree.h> + +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +static void* h_alloc (CfreeHeap* h, size_t n, size_t a) { (void)h;(void)a; return malloc(n); } +static void* h_realloc(CfreeHeap* h, void* p, size_t o, size_t n, size_t a) +{ (void)h;(void)o;(void)a; return realloc(p,n); } +static void h_free (CfreeHeap* h, void* p, size_t n) { (void)h;(void)n; free(p); } +static CfreeHeap g_heap = { h_alloc, h_realloc, h_free, NULL }; + +static void diag_fn(CfreeDiagSink* s, CfreeDiagKind k, CfreeSrcLoc loc, + const char* fmt, va_list ap) +{ + static const char* names[] = { "note","warning","error","fatal" }; + (void)s;(void)loc; + fprintf(stderr, "%s: ", names[k]); vfprintf(stderr, fmt, ap); fputc('\n', stderr); +} +static CfreeDiagSink g_diag = { diag_fn, NULL, 0, 0 }; + +static int slurp(const char* path, uint8_t** out, size_t* len) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + struct stat sb; + if (fstat(fd, &sb) < 0) { close(fd); return -1; } + size_t n = (size_t)sb.st_size; + uint8_t* buf = malloc(n ? n : 1); + if (!buf) { close(fd); return -1; } + size_t got = 0; + while (got < n) { + ssize_t k = read(fd, buf + got, n - got); + if (k <= 0) { free(buf); close(fd); return -1; } + got += (size_t)k; + } + close(fd); *out = buf; *len = n; return 0; +} + +static int write_exe(const char* path, const uint8_t* data, size_t len) +{ + int fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0755); + if (fd < 0) return -1; + size_t w = 0; + while (w < len) { + ssize_t k = write(fd, data + w, len - w); + if (k <= 0) { close(fd); return -1; } + w += (size_t)k; + } + close(fd); return 0; +} + +int main(int argc, char** argv) +{ + const char* out_path = NULL; + const char* entry_name = "_start"; + int gc_sections = 0; + int next_archive = 0; + int next_whole = 0; + + CfreeBytesInput objs[64]; + CfreeBytesInputArchive archives[16]; + uint32_t nobj = 0, narc = 0; + uint8_t* bufs[80]; + int nbufs = 0; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--gc-sections")) { gc_sections = 1; } + else if (!strcmp(argv[i], "--archive")) { next_archive = 1; } + else if (!strcmp(argv[i], "--whole-archive")) { next_whole = 1; next_archive = 1; } + else if (!strcmp(argv[i], "--entry") && i+1 < argc) { entry_name = argv[++i]; } + else if (!strcmp(argv[i], "-o") && i+1 < argc) { out_path = argv[++i]; } + else { + uint8_t* data; size_t len; + if (slurp(argv[i], &data, &len)) { + fprintf(stderr, "link-exe-runner: cannot read %s\n", argv[i]); + return 2; + } + bufs[nbufs++] = data; + if (next_archive) { + CfreeBytesInputArchive* a = &archives[narc++]; + memset(a, 0, sizeof(*a)); + a->input.name = argv[i]; a->input.data = data; a->input.len = len; + a->whole_archive = (uint8_t)next_whole; + next_archive = 0; next_whole = 0; + } else { + CfreeBytesInput* o = &objs[nobj++]; + memset(o, 0, sizeof(*o)); + o->name = argv[i]; o->data = data; o->len = len; + } + } + } + if (!out_path) { fprintf(stderr, "link-exe-runner: missing -o\n"); return 2; } + + CfreeTarget target; + memset(&target, 0, sizeof(target)); + target.arch = CFREE_ARCH_ARM_64; + target.os = CFREE_OS_LINUX; + target.obj = CFREE_OBJ_ELF; + + CfreeEnv env; + memset(&env, 0, sizeof(env)); + env.heap = &g_heap; + env.diag = &g_diag; + + CfreeCompiler* c = cfree_compiler_new(target, &env); + if (!c) { fprintf(stderr, "link-exe-runner: compiler_new failed\n"); return 2; } + + CfreeLinkOptions opts; + memset(&opts, 0, sizeof(opts)); + opts.inputs.obj_bytes = nobj ? objs : NULL; opts.inputs.nobj_bytes = nobj; + opts.inputs.archives = narc ? archives : NULL; opts.inputs.narchives = narc; + opts.inputs.entry = entry_name; + opts.gc_sections = gc_sections; + + CfreeWriter* w = cfree_writer_mem(&g_heap); + if (!w) { cfree_compiler_free(c); return 2; } + + int rc = cfree_link_exe(c, &opts, w); + + for (int i = 0; i < nbufs; i++) free(bufs[i]); + + if (rc) { + cfree_writer_close(w); + cfree_compiler_free(c); + return 1; + } + + size_t out_len; + const uint8_t* out_bytes = cfree_writer_mem_bytes(w, &out_len); + int wrc = write_exe(out_path, out_bytes, out_len); + cfree_writer_close(w); + cfree_compiler_free(c); + if (wrc) { fprintf(stderr, "link-exe-runner: write failed\n"); return 2; } + return 0; +} diff --git a/test/link/harness/start.c b/test/link/harness/start.c @@ -0,0 +1,46 @@ +/* Freestanding _start for Path E (ELF exec) tests. + * + * Convention: + * test_main() — primary test body; returns 0 on pass. + * test_post_fini() — optional post-destructor check (weak default: 0). + * + * Lifecycle: ctors → test_main → dtors → test_post_fini → exit(result). + * + * cfree-ld must define the four boundary symbols as linker-synthesized + * absolute symbols surrounding the sorted init/fini arrays. */ + +extern int test_main(void); +__attribute__((weak)) int test_post_fini(void) { return 0; } + +typedef void (*VoidFn)(void); +extern VoidFn __init_array_start[]; +extern VoidFn __init_array_end[]; +extern VoidFn __fini_array_start[]; +extern VoidFn __fini_array_end[]; + +__attribute__((noreturn)) static void do_exit(int code) +{ + register long x8 __asm__("x8") = 94; /* sys_exit_group */ + register long x0 __asm__("x0") = code; + __asm__ volatile("svc #0" :: "r"(x8), "r"(x0) : "memory"); + __builtin_unreachable(); +} + +void _start(void) +{ + VoidFn* p; + int result; + + for (p = __init_array_start; p != __init_array_end; ++p) + (*p)(); + + result = test_main(); + + for (p = __fini_array_end; p-- != __fini_array_start;) + (*p)(); + + if (result == 0) + result = test_post_fini(); + + do_exit(result); +} diff --git a/test/link/run.sh b/test/link/run.sh @@ -0,0 +1,352 @@ +#!/usr/bin/env bash +# test/link/run.sh — linker and JIT test harness. +# +# Three paths per case: +# +# R test/link/cases/*/ clang-c → .o → cfree-roundtrip → structural diff. +# Validates obj reader + writer fidelity. +# E clang-c → .o → link-exe-runner (cfree_link_exe) → AArch64 ELF +# → qemu/podman → check exit code. +# Validates linker layout and reloc application. +# J clang-c → .o → jit-runner (cfree_link_jit) → call test_main() +# → check return value. +# Validates JIT path (runs on aarch64 host only). +# +# Case markers (files in the case directory): +# expected — expected exit/return value (default 0) +# jit_only — skip R and E paths; run J only +# link_fail — E and J paths expect the link step to fail (non-zero, +# signal-free exit from the runner) +# use_resolver — pass --use-resolver to jit-runner (case 28) +# linker_flags — one flag per line; passed to link-exe-runner and jit-runner +# gc_absent — one symbol per line; jit-runner verifies each is absent +# after --gc-sections (implies linker_flags contains that flag) +# archive_b — package b.o into b.a; content "demand" or "whole" + +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +TEST_DIR="$ROOT/test/link" +BUILD_DIR="$ROOT/build/test" +LIB_AR="$ROOT/build/libcfree.a" +ROUNDTRIP_BIN="$ROOT/build/test/cfree-roundtrip" +NORMALIZE="$ROOT/test/elf/normalize.py" + +LINK_EXE_RUNNER="$BUILD_DIR/link-exe-runner" +JIT_RUNNER="$BUILD_DIR/jit-runner" + +CLANG_TARGET="--target=aarch64-linux-gnu" +CC="${CC:-cc}" +CFREE_CFLAGS="-I$ROOT/include" +ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" + +mkdir -p "$BUILD_DIR" +mkdir -p "$BUILD_DIR/link" + +PASS=0; FAIL=0; SKIP=0 +FAIL_NAMES=(); SKIP_NAMES=() + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } +note_fail() { FAIL=$((FAIL+1)); FAIL_NAMES+=("$1"); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } +note_skip() { SKIP=$((SKIP+1)); SKIP_NAMES+=("$1"); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } + +# ---- tool detection -------------------------------------------------------- + +have_clang_cross=0 +have_readelf=0 +have_python3=0 +have_qemu=0 +have_podman=0 +have_runner=0 +have_ar=0 +have_roundtrip=0 +is_aarch64=0 + +if clang $CLANG_TARGET -x c - -o /dev/null < /dev/null 2>/dev/null; then + have_clang_cross=1 +fi +command -v llvm-readelf >/dev/null 2>&1 && have_readelf=1 +command -v readelf >/dev/null 2>&1 && have_readelf=1 +command -v python3 >/dev/null 2>&1 && have_python3=1 +command -v ar >/dev/null 2>&1 && have_ar=1 +[ -f "$ROUNDTRIP_BIN" ] && have_roundtrip=1 + +QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" +[ -n "$QEMU_BIN" ] && have_qemu=1 +command -v podman >/dev/null 2>&1 && have_podman=1 +{ [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; } && have_runner=1 + +arch_raw="$(uname -m 2>/dev/null || true)" +{ [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 + +READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || true)" +RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}" + +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")" + 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 +} + +# ---- build harness binaries ------------------------------------------------ + +printf 'Building harness...\n' + +if [ ! -f "$LIB_AR" ]; then + printf ' FATAL: %s not found — run "make lib" first\n' "$LIB_AR" >&2 + exit 1 +fi + +have_exe_runner=0 +have_jit_runner=0 + +if $CC $CFREE_CFLAGS "$TEST_DIR/harness/link_exe_runner.c" \ + "$LIB_AR" -o "$LINK_EXE_RUNNER" 2>"$BUILD_DIR/link-exe-runner.err"; then + have_exe_runner=1 + printf ' %s link-exe-runner\n' "$(color_grn built)" +else + printf ' %s link-exe-runner (see %s)\n' \ + "$(color_yel warn)" "$BUILD_DIR/link-exe-runner.err" >&2 +fi + +if [ $is_aarch64 -eq 1 ]; then + if $CC $CFREE_CFLAGS "$TEST_DIR/harness/jit_runner.c" \ + "$LIB_AR" -o "$JIT_RUNNER" 2>"$BUILD_DIR/jit-runner.err"; then + have_jit_runner=1 + printf ' %s jit-runner\n' "$(color_grn built)" + else + printf ' %s jit-runner (see %s)\n' \ + "$(color_yel warn)" "$BUILD_DIR/jit-runner.err" >&2 + fi +fi + +printf 'Running cases...\n' + +# ---- per-case loop --------------------------------------------------------- + +for case_dir in "$TEST_DIR/cases"/*/; do + [ -d "$case_dir" ] || continue + name="$(basename "$case_dir")" + work="$BUILD_DIR/link/$name" + mkdir -p "$work" + + # Read markers + expected=0; [ -f "$case_dir/expected" ] && expected="$(cat "$case_dir/expected" | tr -d '[:space:]')" + jit_only=0; [ -f "$case_dir/jit_only" ] && jit_only=1 + link_fail=0; [ -f "$case_dir/link_fail" ] && link_fail=1 + use_resolver=0; [ -f "$case_dir/use_resolver" ] && use_resolver=1 + archive_mode="none" + if [ -f "$case_dir/archive_b" ]; then + archive_mode="$(cat "$case_dir/archive_b" | tr -d '[:space:]')" + fi + + # Collect extra linker flags + extra_flags=() + if [ -f "$case_dir/linker_flags" ]; then + while IFS= read -r flag; do + [ -n "$flag" ] && extra_flags+=("$flag") + done < "$case_dir/linker_flags" + fi + + # Collect GC-absent symbols + gc_absent_syms=() + if [ -f "$case_dir/gc_absent" ]; then + while IFS= read -r sym; do + [ -n "$sym" ] && gc_absent_syms+=("$sym") + done < "$case_dir/gc_absent" + fi + + # Collect source files + tu_srcs=() + for f in "$case_dir/a.c" "$case_dir/b.c" "$case_dir/c.c"; do + [ -f "$f" ] && tu_srcs+=("$f") + done + + # ---- compile with clang cross ------------------------------------------ + if [ $have_clang_cross -eq 0 ]; then + note_skip "$name/R" "no aarch64 clang" + [ $jit_only -eq 0 ] && note_skip "$name/E" "no aarch64 clang" + note_skip "$name/J" "no aarch64 clang" + continue + fi + + obj_files=(); compile_ok=1 + for src in "${tu_srcs[@]}"; do + base="$(basename "$src" .c)" + obj="$work/${base}.o" + if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \ + -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then + compile_ok=0; break + fi + obj_files+=("$obj") + done + + if [ $compile_ok -eq 0 ]; then + note_fail "$name (compile failed)" + continue + fi + + # ---- build archive from b.o if requested -------------------------------- + # Roundtrip always uses raw .o files; exec/JIT use the archive. + rt_obj_files=("${obj_files[@]}") # for roundtrip: all .o files + link_obj_files=() # for exec/JIT + link_arc_flags=() + + for o in "${obj_files[@]}"; do + base="$(basename "$o" .o)" + if [ "$base" = "b" ] && [ "$archive_mode" != "none" ]; then + if [ $have_ar -eq 1 ]; then + arc="$work/b.a" + ar rcs "$arc" "$o" 2>/dev/null + if [ "$archive_mode" = "whole" ]; then + link_arc_flags+=(--whole-archive --archive "$arc") + else + link_arc_flags+=(--archive "$arc") + fi + else + # no ar: just pass .o directly (archive test degrades to obj test) + link_obj_files+=("$o") + fi + else + link_obj_files+=("$o") + fi + done + + # ---- Path R: roundtrip -------------------------------------------------- + if [ $jit_only -eq 0 ]; then + if [ $have_roundtrip -eq 1 ] && [ $have_readelf -eq 1 ] && [ $have_python3 -eq 1 ]; then + r_ok=1 + for obj in "${rt_obj_files[@]}"; do + base="$(basename "$obj" .o)" + rt="$work/${base}_rt.o" + if ! "$ROUNDTRIP_BIN" "$obj" "$rt" 2>"$work/rt_${base}.err"; then + r_ok=0; break + fi + "$READELF_BIN" -aW "$obj" | python3 "$NORMALIZE" \ + >"$work/${base}_golden.norm" 2>/dev/null + "$READELF_BIN" -aW "$rt" | python3 "$NORMALIZE" \ + >"$work/${base}_rt.norm" 2>/dev/null + if ! diff -u "$work/${base}_golden.norm" \ + "$work/${base}_rt.norm" \ + >"$work/${base}_diff.txt" 2>&1; then + r_ok=0; break + fi + done + if [ $r_ok -eq 1 ]; then note_pass "$name/R" + else note_fail "$name/R"; fi + else + note_skip "$name/R" "missing roundtrip/readelf/python3" + fi + fi + + # ---- Path E: exec ------------------------------------------------------- + if [ $jit_only -eq 0 ] && [ $have_exe_runner -eq 1 ]; then + # Compile start.o + start_obj="$work/start.o" + clang $CLANG_TARGET -O1 -ffreestanding -fno-stack-protector \ + -c "$TEST_DIR/harness/start.c" -o "$start_obj" 2>/dev/null + + exe="$work/linked.exe" + link_cmd=("$LINK_EXE_RUNNER" "${extra_flags[@]}" -o "$exe" \ + "${link_obj_files[@]}" "$start_obj" "${link_arc_flags[@]}") + + if [ $link_fail -eq 1 ]; then + if "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then + note_fail "$name/E (expected link failure but link succeeded)" + else + e_rc=$? + # Clean fail: non-zero and no signal (exit < 128) + if [ $e_rc -lt 128 ]; then note_pass "$name/E" + else note_fail "$name/E (link failed via signal $e_rc)"; fi + fi + else + if ! "${link_cmd[@]}" >"$work/exec_link.out" 2>"$work/exec_link.err"; then + note_fail "$name/E (link failed)" + elif [ $have_runner -eq 1 ]; then + run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" + if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E" + else note_fail "$name/E (expected $expected, got $RUN_RC)"; fi + else + note_skip "$name/E" "no runner (qemu/podman)" + fi + fi + else + if [ $jit_only -eq 0 ]; then + note_skip "$name/E" "no link-exe-runner" + fi + fi + + # ---- Path J: JIT -------------------------------------------------------- + if [ $have_jit_runner -eq 1 ]; then + jit_cmd=("$JIT_RUNNER" "${extra_flags[@]}") + [ $use_resolver -eq 1 ] && jit_cmd+=(--use-resolver) + jit_cmd+=("${link_obj_files[@]}" "${link_arc_flags[@]}") + + if [ $link_fail -eq 1 ]; then + if "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err"; then + note_fail "$name/J (expected link failure but link succeeded)" + else + j_rc=$? + if [ $j_rc -lt 128 ]; then note_pass "$name/J" + else note_fail "$name/J (link failed via signal $j_rc)"; fi + fi + else + "${jit_cmd[@]}" >"$work/jit.out" 2>"$work/jit.err" + j_rc=$? + + # For gc_sections cases, additionally verify absent symbols. + # The jit_runner returns 0 on test_main pass; absent-symbol + # verification is handled by the jit_runner reading gc_absent + # via a side channel. Since the runner doesn't read case markers + # itself, we instead check the lookup from a thin wrapper here: + # re-run jit_runner with --check-absent SYM for each gc_absent sym. + for sym in "${gc_absent_syms[@]}"; do + if "${jit_cmd[@]}" --check-absent "$sym" \ + >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then + : # absent check passed + else + j_rc=$? + break + fi + done + + if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J" + else note_fail "$name/J (expected $expected, got $j_rc)"; fi + fi + else + note_skip "$name/J" "no jit-runner (not aarch64 host or build failed)" + fi + +done + +# ---- summary --------------------------------------------------------------- + +printf '\n' +printf 'Results: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + +if [ ${#FAIL_NAMES[@]} -gt 0 ]; then + printf 'Failed:\n' + for n in "${FAIL_NAMES[@]}"; do printf ' %s\n' "$n"; done +fi + +if [ ${#SKIP_NAMES[@]} -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then + printf 'Skipped (treat as failure; set CFREE_TEST_ALLOW_SKIP=1 to allow):\n' + for n in "${SKIP_NAMES[@]}"; do printf ' %s\n' "$n"; done +fi + +if [ $FAIL -gt 0 ]; then exit 1; fi +if [ $SKIP -gt 0 ] && [ "$ALLOW_SKIP" != "1" ]; then exit 1; fi +exit 0 diff --git a/test/test.mk b/test/test.mk @@ -9,10 +9,13 @@ # libcfree.a. Set CFREE_AR_TEST_HOST=1 to also dump produced bytes # to /tmp and run the host's `ar t` / `nm --print-armap` as a # cross-check. +# - test-link: linker + JIT behavioral harness in test/link/; three paths +# per case (roundtrip R, ELF exec E, JIT J). Depends only on libcfree.a. +# Set CFREE_TEST_ALLOW_SKIP=1 to allow skipped layers. -.PHONY: test test-lex test-pp test-pp-err test-elf test-ar +.PHONY: test test-lex test-pp test-pp-err test-elf test-ar test-link -test: test-lex test-pp test-pp-err test-elf test-ar +test: test-lex test-pp test-pp-err test-elf test-ar test-link test-lex: bin @CFREE=$(abspath $(BIN)) test/lex/run.sh @@ -42,3 +45,6 @@ test-ar: $(AR_TEST_BIN) $(AR_TEST_BIN): test/ar_test.c $(LIB_AR) @mkdir -p $(dir $@) $(CC) $(DRIVER_CFLAGS) test/ar_test.c $(LIB_AR) -o $@ + +test-link: lib + bash test/link/run.sh