test/link — Linker and JIT test corpus
Unified behavioral test suite for kit'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, kit-roundtrip, llvm-readelf, python3 |
| E exec |
kit_link_exe layout + reloc application; runs via qemu/podman |
clang cross, link-exe-runner, qemu or podman |
| J JIT |
kit_link_jit + kit_jit_*; runs natively on aarch64 host |
clang cross, jit-runner (aarch64 only) |
Convention
Every case exposes:
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 kit_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 |
use_resolver |
pass --use-resolver to jit_runner |
linker_flags |
one flag per line passed to link-exe-runner and jit-runner |
cflags |
extra clang -c flags appended to every TU compile in the case |
gc_absent |
one symbol per line that must be absent post-link (e.g. dropped by --gc-sections) |
gc_present |
one symbol per line that must remain present post-link |
archive_b |
package b.o as b.a; content demand (normal) or whole (--whole-archive) |
linker_script |
basename of an .lds file in the case dir; passed to both runners via --linker-script |
kernel_image |
empty marker; case is a freestanding kernel image. Skips R and J; on E, runs the linked exe under qemu-system-aarch64 -kernel with semihosting |
Negative tests live in test/link/bad/<name>/ instead of cases/. Each
bad-case directory contains source files (compile cleanly) plus an
expect file: a substring that must appear in the runner's stderr.
Pass = runner exits non-zero with no signal AND stderr contains
expect. The bad/ location is the marker — no link_fail flag.
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 |
| 25a |
gc_basic |
--gc-sections + -ffunction-sections; unreachable fn dropped |
| 25b |
no_gc_baseline |
same source as 25a, no --gc-sections; unreachable fn survives |
| 25c |
gc_data |
-fdata-sections; unreferenced global data dropped |
| 25d |
gc_chain |
transitive closure: live A→B→C kept; orphan triple dropped |
| 25e |
gc_keep_init_array |
constructor with no static caller survives via .init_array root |
| 25f |
gc_retain |
SHF_GNU_RETAIN (__attribute__((retain))) survives GC |
| 25g |
gc_keeps_data_via_reloc |
live fn references global; data section kept by reloc edge |
| 25h |
gc_start_stop |
__start_X/__stop_X retain section X and resolve to its span |
| 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 |
KitExternResolver provides address for undefined symbol |
| 29 |
jit_lookup_miss |
kit_jit_lookup returns NULL for unknown name |
| 32 |
ifunc |
STT_GNU_IFUNC trampoline: iplt stub + igot slot, resolver invoked at JIT load |
Group F — TLS
| # |
Name |
Exercises |
| 31 |
tls_local_exec |
_Thread_local w/ initializer; R_AARCH64_TLSLE_ADD_TPREL_{HI12,LO12_NC} apply + PT_TLS layout |
Group G — linker scripts
| # |
Name |
Exercises |
| 35 |
linker_script_kernel |
ENTRY, SECTIONS { . = 0x40080000; .text/.rodata/.data/.bss with ALIGN; __bss_start, _end; /DISCARD/of.note.*, .comment, .eh_frame. Linked image boots under qemu-system-aarch64 -kernel` and exits via ARM semihosting. |
Group H — Mach-O atoms
| # |
Name |
Exercises |
| 40 |
macho_gc_dead_atom_reloc |
GC skips relocations from dead Mach-O subsections-via-symbols atoms |
| 41 |
macho_many_atom_relocs |
Many Mach-O function atoms and call relocations under --gc-sections |
bad/ — negative tests
| # |
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):
kit_jit_from_image must execute init_array ctors after mprotect
(needed by cases 20–23, 27).
kit_jit_run_dtors must walk fini_array sections in reverse
(needed by cases 21–22).
kit-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).
kit-ld must sort .init_array.NNN sections by priority suffix
(needed by case 23).
kit_link_exe / kit_link_jit must support KitLinkOptions.gc_sections
(needed by case 25).
- Archive reader (
link_add_archive_bytes) must support demand-loading
and whole_archive (needed by cases 26–27).
kit_link_jit must return non-zero on unresolved strong symbols
without extern_resolver (needed by case 30).
link_reloc_apply must handle R_AARCH64_TLSLE_ADD_TPREL_HI12 and
R_AARCH64_TLSLE_ADD_TPREL_LO12_NC; kit_link_exe must emit a
PT_TLS segment for .tdata/.tbss; harness _start must set
up TPIDR_EL0 (needed by case 31).