kit

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

commit 6fd6c34fe44b2dfe41dd0158a2819a725c1afeed
parent 7b9993d49daf2887b1c1b9ba652806821d1ef0fe
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat,  9 May 2026 13:13:45 -0700

link: implement --gc-sections (entry/init-fini/RETAIN/start-stop roots)

Section-granularity GC over input (input_idx, ObjSecId) pairs. Roots:
the entry symbol's section, every init/fini/preinit_array section,
SHF_GNU_RETAIN sections, and the encoding-section X for any live
__start_<X>/__stop_<X> reference. Edges follow per-section relocs to
fixed point. Dropped sections never get a LinkSectionId; their
defining symbols have `defined` cleared so cfree_jit_lookup returns
NULL. Pass runs after resolve_symbols, before layout_sections.

- src/obj: add SF_RETAIN; decode SHF_GNU_RETAIN in elf_read; encode in
  elf_emit; bump EI_OSABI to ELFOSABI_GNU when any section carries it
  (matches clang for byte-stable roundtrip).
- src/link/link_layout.c: gc_compute / gc_drop_dead_globals; generalize
  emit_array_boundaries into emit_encoding_section_boundaries so
  __start_<id>/__stop_<id> resolve to the section span.
- driver/ld.c: --gc-sections / --no-gc-sections; help text.
- test/link: per-case `cflags` marker; `gc_present` mirrors `gc_absent`;
  jit_runner gains --check-present; E-path readelf check is wired but
  noop until cfree-link-exe emits a symtab. Replace 25_gc_sections with
  focused 25a..25h subgroup, one liveness rule per case.
- doc/linker-status.md: refreshed.

COMDAT-group atomicity is the one root rule deferred — no current case
emits SHF_GROUP, and the rule plugs into the same worklist when it does.

test-link: 106/106 pass.

Diffstat:
Mdoc/linker-status.md | 89++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mdriver/ld.c | 14+++++++++++++-
Msrc/link/link_layout.c | 401+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/obj/elf.h | 1+
Msrc/obj/elf_emit.c | 17++++++++++++++---
Msrc/obj/elf_read.c | 4+++-
Msrc/obj/obj.h | 1+
Mtest/link/CORPUS.md | 13+++++++++++--
Dtest/link/cases/25_gc_sections/a.c | 8--------
Atest/link/cases/25a_gc_basic/a.c | 8++++++++
Atest/link/cases/25a_gc_basic/cflags | 1+
Rtest/link/cases/25_gc_sections/gc_absent -> test/link/cases/25a_gc_basic/gc_absent | 0
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25a_gc_basic/linker_flags | 0
Atest/link/cases/25b_no_gc_baseline/a.c | 7+++++++
Atest/link/cases/25b_no_gc_baseline/cflags | 1+
Rtest/link/cases/25_gc_sections/gc_absent -> test/link/cases/25b_no_gc_baseline/gc_present | 0
Atest/link/cases/25c_gc_data/a.c | 7+++++++
Atest/link/cases/25c_gc_data/cflags | 1+
Atest/link/cases/25c_gc_data/gc_absent | 1+
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25c_gc_data/linker_flags | 0
Atest/link/cases/25d_gc_chain/a.c | 19+++++++++++++++++++
Atest/link/cases/25d_gc_chain/cflags | 1+
Atest/link/cases/25d_gc_chain/gc_absent | 3+++
Atest/link/cases/25d_gc_chain/gc_present | 3+++
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25d_gc_chain/linker_flags | 0
Atest/link/cases/25e_gc_keep_init_array/a.c | 9+++++++++
Atest/link/cases/25e_gc_keep_init_array/cflags | 1+
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25e_gc_keep_init_array/linker_flags | 0
Atest/link/cases/25f_gc_retain/a.c | 7+++++++
Atest/link/cases/25f_gc_retain/cflags | 1+
Atest/link/cases/25f_gc_retain/gc_present | 1+
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25f_gc_retain/linker_flags | 0
Atest/link/cases/25g_gc_keeps_data_via_reloc/a.c | 7+++++++
Atest/link/cases/25g_gc_keeps_data_via_reloc/cflags | 1+
Atest/link/cases/25g_gc_keeps_data_via_reloc/gc_present | 1+
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25g_gc_keeps_data_via_reloc/linker_flags | 0
Atest/link/cases/25h_gc_start_stop/a.c | 20++++++++++++++++++++
Rtest/link/cases/25_gc_sections/linker_flags -> test/link/cases/25h_gc_start_stop/linker_flags | 0
Mtest/link/harness/jit_runner.c | 20++++++++++++++++++--
Mtest/link/run.sh | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
40 files changed, 678 insertions(+), 78 deletions(-)

diff --git a/doc/linker-status.md b/doc/linker-status.md @@ -16,29 +16,16 @@ live in `test/link/` — they are not duplicated in `test/elf/`. | Harness | Pass | Fail | Notes | |-----------------|-----:|-----:|--------------------------------------| | `test-elf` | 37 | 0 | All Layer A/B/C green | -| `test-link` R | 26 | 0 | object roundtrip via cfree-roundtrip | -| `test-link` E | 25 | 2 | archives only (26, 27) | -| `test-link` J | 23 | 3 | archives + 25_gc_sections | +| `test-link` R | 33 | 0 | object roundtrip via cfree-roundtrip | +| `test-link` E | 33 | 0 | qemu/podman aarch64 exec | +| `test-link` J | 33 | 0 | JIT in-process incl. GC subgroup | | `test-link` bad | 2 | 0 | `bad/30_undef_strong` (E + J) | (R = roundtrip; E = link → aarch64 ELF → qemu/podman; J = JIT in-process.) ---- - -## test-link failures — root causes - -### Archive ingestion (E + J) - -| Case | Symptom | Root cause | -|------|---------|------------| -| `26_archive_demand` | `link_add_archive_bytes: not yet implemented` | archive ingestion is a stub | -| `27_archive_whole` | same | same | - -### `--gc-sections` (J only; E passes because the test only exits 0) - -| Case | What's missing | -|------|----------------| -| `25_gc_sections` | linker accepts `--gc-sections` but doesn't actually drop unreferenced sections. The J path's `--check-absent unreachable_fn` correctly observes the symbol is still present. Section-granularity GC also requires the harness to compile cases with `-ffunction-sections`/`-fdata-sections` so symbols land in their own sections. | +All test-link cases currently pass. Archive ingestion and `--gc-sections` +are now functional; case `25_gc_sections` was split into `25a..25h`, one +case per liveness rule (see Recently landed). --- @@ -87,21 +74,53 @@ already arm64; this kept the E path at ~200ms/case. ### Linker -1. **Archive ingestion** — implement `link_add_archive_bytes`; wire - `cfree_ar_iter` for both demand-load and `--whole-archive`. Cases - `26_archive_demand`, `27_archive_whole`. Scaffolding (LinkArchive, - archives_grow, archive parsing in link_add_archive_bytes) is in - place; the inclusion pass in link_resolve (`link_ingest_archives`) - is unimplemented. -2. **`--gc-sections`** — currently accepted-but-ignored. To actually - drop `unreachable_fn` in case 25, walk live-set from entry + - init_array / fini_array roots, mark sections reachable through - relocs, drop the rest at layout time. Also requires the harness - to compile cases with `-ffunction-sections -fdata-sections` so - symbols land in their own droppable sections. +1. **COMDAT group atomicity for `--gc-sections`** — `SHF_GROUP` + members are currently treated independently. C-only inputs don't + exercise this; C++ inline / weak-template inputs would. When the + first such case lands, extend the GC pass: when any member of a + group becomes live, all members of the same `ObjGroup` follow. +2. **Symtab in cfree-link-exe** — the executable writer emits PHDRs + only (no `.symtab` / `.strtab`). The E-path readelf-based + `gc_absent` / `gc_present` verifier in `test/link/run.sh` is + already wired; once a symtab is emitted the checks will activate + without further harness changes. +3. **`-ffunction-sections` / `-fdata-sections` from cfree's own + compiler.** `arch.h:254` notes that `text_section_id` is + per-function so the model is in place. Self-hosted GC tests + would compose `cfree cc -ffunction-sections … -c` with `cfree ld + --gc-sections`. Not yet exercised. ## Recently landed +- **`--gc-sections`** (full, except COMDAT atomicity). Section + granularity is the input `(input_idx, ObjSecId)` pair. Roots: + the entry symbol's section, every `SSEM_INIT_ARRAY` / + `FINI_ARRAY` / `PREINIT_ARRAY`, and `SF_RETAIN` (`SHF_GNU_RETAIN`, + i.e. `__attribute__((retain))`). Edges follow per-section relocs; + references to `__start_<X>` / `__stop_<X>` additionally promote + every section named `<X>`. After layout, defs in dropped sections + are cleared (`defined = 0`) so `cfree_jit_lookup` returns NULL. + Implementation in `link_layout.c:gc_compute` / + `gc_drop_dead_globals`; pass runs after `resolve_symbols`, before + `layout_sections`. +- **`__start_<X>` / `__stop_<X>` boundary synthesis.** Generalized + from the init/fini-only `emit_array_boundaries`. Any undef sym + whose name parses as `__start_<id>` / `__stop_<id>` resolves to + the low / high vaddr of every output `LinkSection` whose source + was named `<id>`. Cases: `25h_gc_start_stop`. +- **`SHF_GNU_RETAIN`.** New `SF_RETAIN` flag in `obj.h`; decoded in + `elf_read.c`, encoded in `elf_emit.c`; emitter also bumps + `EI_OSABI` to `ELFOSABI_GNU` when any section carries it + (matching clang's behavior for byte-stable roundtrip). +- **`cfree ld --gc-sections` / `--no-gc-sections`** in `driver/ld.c`. +- **Test corpus expansion.** `25_gc_sections` split into + `25a_gc_basic` … `25h_gc_start_stop`, one rule per case. New + per-case `cflags` marker lets a case ask for + `-ffunction-sections` / `-fdata-sections` without affecting other + cases. New `gc_present` marker (mirror of `gc_absent`); `jit-runner` + gained `--check-present SYM`. E-path readelf-based + `gc_absent`/`gc_present` checks are wired but skip silently when + the exe has no symtab. - **Static GOT** for `R_AARCH64_ADR_GOT_PAGE` / `R_AARCH64_LD64_GOT_LO12_NC`: `layout_got` collects unique GOT-needing symbols, appends a synthetic `.got` segment carrying @@ -115,10 +134,14 @@ already arm64; this kept the E path at ~200ms/case. is the last section in its segment) now resolve. Fixes cases `21_fini_array`, `22_init_fini_both`, `23_init_order` on the J path. +- **Archive ingestion** (`link_add_archive_bytes` + + `link_ingest_archives`): demand-load and `--whole-archive` both + iterate to a fixed point. Fixes `26_archive_demand`, + `27_archive_whole`. - **JIT runner** already invokes `.init_array` (in `cfree_jit_from_image`), `cfree_jit_run_dtors` for `.fini_array`, - and `test_post_fini` after `test_main`. `--check-absent SYM` is - wired for the gc-sections verification. + and `test_post_fini` after `test_main`. `--check-absent SYM` and + `--check-present SYM` cover the gc-sections verification. --- diff --git a/driver/ld.c b/driver/ld.c @@ -26,6 +26,7 @@ * -E / --export-dynamic promote all defined globals to the dynamic symbol table * --whole-archive / --no-whole-archive * positional state for following .a + * --gc-sections / --no-gc-sections drop unreferenced sections * -Bstatic / -Bdynamic positional link-mode for following .a * --as-needed / --no-as-needed positional link-mode for following .a * --start-group / --end-group cyclic-resolution group of archives @@ -74,6 +75,7 @@ typedef struct LdOptions { uint32_t nrpath_links; int new_dtags; /* 1=DT_RUNPATH (default), 0=DT_RPATH */ int export_dynamic; /* -E / --export-dynamic */ + int gc_sections; /* --gc-sections / --no-gc-sections */ /* --build-id state */ uint8_t build_id_mode; /* CfreeBuildIdMode */ @@ -147,6 +149,12 @@ void driver_help_ld(void) " -rpath-link DIR Link-time-only search path (advisory)\n" " --enable-new-dtags rpaths land in DT_RUNPATH (default)\n" " --disable-new-dtags rpaths land in DT_RPATH\n" + "\n" + "SECTION GC\n" + " --gc-sections Drop sections unreferenced from the entry\n" + " symbol, init/fini arrays, SHF_GNU_RETAIN,\n" + " and __start_/__stop_ section references\n" + " --no-gc-sections Disable section GC (default)\n" " -E, --export-dynamic Promote defined globals into dynsym\n" " (no-op for -shared; recorded for exe)\n" "\n" @@ -350,6 +358,8 @@ static int ld_parse(int argc, char** argv, LdOptions* o) } if (driver_streq(a, "--enable-new-dtags")) { o->new_dtags = 1; continue; } if (driver_streq(a, "--disable-new-dtags")) { o->new_dtags = 0; continue; } + if (driver_streq(a, "--gc-sections")) { o->gc_sections = 1; continue; } + if (driver_streq(a, "--no-gc-sections")) { o->gc_sections = 0; continue; } if (driver_streq(a, "-E") || driver_streq(a, "--export-dynamic")) { o->export_dynamic = 1; @@ -648,6 +658,7 @@ static int ld_run_link(LdOptions* o) /* allow_undefined defaults to 1 for shared output (the produced * object resolves against its loader at runtime). */ shared_opts.allow_undefined = 1; + shared_opts.gc_sections = o->gc_sections; /* -E/--export-dynamic is exe-shaped (promote globals into the * dynsym of an executable). For shared output every defined global * is already exported, so the flag is a no-op here; we accept it @@ -657,7 +668,8 @@ static int ld_run_link(LdOptions* o) } else { CfreeLinkOptions zero = {0}; link_opts = zero; - link_opts.inputs = inputs; + link_opts.inputs = inputs; + link_opts.gc_sections = o->gc_sections; if (o->export_dynamic) { /* TODO(#5/exe): once CfreeLinkOptions grows an export_dynamic * field (or per-symbol export list for executables), wire it diff --git a/src/link/link_layout.c b/src/link/link_layout.c @@ -317,6 +317,311 @@ static void resolve_undefs(Linker* l, LinkImage* img) } } +/* ---- pass 1b: --gc-sections liveness ---- + * + * Granularity is the input section: pairs (input_idx, ObjSecId). + * Roots: + * 1. The section defining the entry symbol. + * 2. Every SSEM_INIT_ARRAY / SSEM_FINI_ARRAY / SSEM_PREINIT_ARRAY + * (these are KEEP() in standard linker scripts). + * 3. SF_RETAIN sections (SHF_GNU_RETAIN, i.e. clang's + * __attribute__((retain)) / used). + * Edges: + * For each live section, every reloc whose target sym has a + * defining section pulls that section live. References whose + * target name is __start_<X> / __stop_<X> with valid C-identifier + * <X> additionally pull every section named <X> live. + * Iterates to a fixed point. When l->gc_sections is 0, every kept + * section is marked live unconditionally so downstream passes share + * a single "is this section live?" predicate. + * + * The mark table is consumed by layout_sections (skips dead sections), + * by gc_drop_dead_globals (clears `defined` on syms whose section was + * dropped), and indirectly by emit_reloc_records / layout_got (which + * filter through m->section[j] == LINK_SEC_NONE since dead sections + * never get a LinkSectionId). */ + +typedef struct GcLive { + u8** marks; /* marks[input_idx][obj_sec_id]; 0/1, sized to nsec_per_input[ii] */ + u32* nsec; /* obj_section_count per input */ + u32 ninputs; +} GcLive; + +typedef struct GcQueue { + u64* items; /* (u64) packed: hi32 = input_idx, lo32 = obj_sec_id */ + u32 n; + u32 cap; +} GcQueue; + +#define GC_PACK(ii, j) (((u64)(u32)(ii) << 32) | (u32)(j)) +#define GC_II(p) ((u32)((p) >> 32)) +#define GC_J(p) ((ObjSecId)((p) & 0xffffffffu)) + +static void gc_queue_push(GcQueue* q, Heap* h, u32 ii, ObjSecId j) +{ + if (q->n == q->cap) { + u32 nc = q->cap ? q->cap * 2u : 32u; + u64* nb = (u64*)h->realloc(h, q->items, + sizeof(*q->items) * q->cap, + sizeof(*q->items) * nc, _Alignof(u64)); + if (!nb) return; /* skip; caller iterates to fixed point */ + q->items = nb; + q->cap = nc; + } + q->items[q->n++] = GC_PACK(ii, j); +} + +static void gc_live_alloc(GcLive* g, Linker* l, Heap* h) +{ + u32 ii; + g->ninputs = l->ninputs; + g->marks = l->ninputs + ? (u8**)h->alloc(h, sizeof(*g->marks) * l->ninputs, _Alignof(u8*)) + : NULL; + g->nsec = l->ninputs + ? (u32*)h->alloc(h, sizeof(*g->nsec) * l->ninputs, _Alignof(u32)) + : NULL; + if (l->ninputs && (!g->marks || !g->nsec)) + compiler_panic(l->c, no_loc(), "link: oom on gc live map"); + for (ii = 0; ii < l->ninputs; ++ii) { + u32 nsec = obj_section_count(l->inputs[ii].obj); + g->nsec[ii] = nsec; + g->marks[ii] = (u8*)h->alloc(h, nsec ? nsec : 1u, 1); + if (!g->marks[ii]) + compiler_panic(l->c, no_loc(), "link: oom on gc marks"); + memset(g->marks[ii], 0, nsec); + } +} + +static void gc_live_free(GcLive* g, Heap* h) +{ + u32 ii; + if (g->marks) { + for (ii = 0; ii < g->ninputs; ++ii) + if (g->marks[ii]) h->free(h, g->marks[ii], g->nsec[ii] ? g->nsec[ii] : 1u); + h->free(h, g->marks, sizeof(*g->marks) * g->ninputs); + } + if (g->nsec) h->free(h, g->nsec, sizeof(*g->nsec) * g->ninputs); +} + +static int gc_live_get(const GcLive* g, u32 ii, ObjSecId j) +{ + if (ii >= g->ninputs || j == OBJ_SEC_NONE || j >= g->nsec[ii]) return 0; + return g->marks[ii][j]; +} + +static void gc_mark(GcLive* g, GcQueue* q, Heap* h, u32 ii, ObjSecId j) +{ + if (ii >= g->ninputs || j == OBJ_SEC_NONE || j >= g->nsec[ii]) return; + if (g->marks[ii][j]) return; + g->marks[ii][j] = 1; + gc_queue_push(q, h, ii, j); +} + +/* From a LinkSymId, find the (input_idx, obj_sec_id) of its defining + * section. Returns 1 on hit. Recurses one level through img->globals + * for undef symbols whose name resolves to a global definition. */ +static int gc_def_site(LinkImage* img, Linker* l, LinkSymId id, + u32* out_ii, ObjSecId* out_sid) +{ + const LinkSymbol* s; + ObjBuilder* ob; + const ObjSym* osym; + if (id == LINK_SYM_NONE || id > img->nsyms) return 0; + s = &img->syms[id - 1]; + if (!s->defined) { + LinkSymId hit; + if (s->name == 0) return 0; + hit = symhash_get(&img->globals, s->name); + if (hit == LINK_SYM_NONE || hit == s->id) return 0; + return gc_def_site(img, l, hit, out_ii, out_sid); + } + if (s->kind == SK_ABS || s->kind == SK_COMMON) return 0; + if (s->input_id == LINK_INPUT_NONE) return 0; /* synthesized */ + ob = l->inputs[s->input_id - 1].obj; + osym = obj_symbol_get(ob, s->obj_sym); + if (!osym || osym->section_id == OBJ_SEC_NONE) return 0; + *out_ii = (u32)(s->input_id - 1u); + *out_sid = osym->section_id; + return 1; +} + +/* Detect __start_<X> / __stop_<X> with <X> a valid C identifier. + * On hit, *out_off is the offset of <X> within `s`, *out_len its + * length, and *out_is_start is 1 for __start_ / 0 for __stop_. + * out_is_start may be NULL when the caller doesn't need to + * distinguish (e.g. GC, which retains for either prefix). */ +static int gc_split_start_stop(const char* s, size_t n, + size_t* out_off, size_t* out_len, + int* out_is_start) +{ + static const char START[] = "__start_"; + static const char STOP[] = "__stop_"; + size_t off, len, i; + int is_start; + if (n > sizeof(START) - 1u && memcmp(s, START, sizeof(START) - 1u) == 0) { + off = sizeof(START) - 1u; + is_start = 1; + } else if (n > sizeof(STOP) - 1u && memcmp(s, STOP, sizeof(STOP) - 1u) == 0) { + off = sizeof(STOP) - 1u; + is_start = 0; + } else { + return 0; + } + len = n - off; + if (len == 0) return 0; + { + char c = s[off]; + if (!(c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) + return 0; + } + for (i = 1; i < len; ++i) { + char c = s[off + i]; + if (!(c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9'))) + return 0; + } + *out_off = off; + *out_len = len; + if (out_is_start) *out_is_start = is_start; + return 1; +} + +static void gc_promote_by_section_name(Linker* l, GcLive* g, GcQueue* q, + Heap* h, Sym section_name) +{ + u32 ii, j; + for (ii = 0; ii < l->ninputs; ++ii) { + ObjBuilder* ob = l->inputs[ii].obj; + u32 nsec = obj_section_count(ob); + for (j = 1; j < nsec; ++j) { + const Section* s = obj_section_get(ob, j); + if (!s || !section_kept(s)) continue; + if (s->name != section_name) continue; + gc_mark(g, q, h, ii, j); + } + } +} + +static void gc_compute(Linker* l, LinkImage* img, GcLive* g) +{ + u32 ii, j, k; + GcQueue q; + Heap* h = img->heap; + + /* GC disabled: every kept section becomes live. Downstream passes + * use the same is-live predicate, so this keeps logic uniform. */ + if (!l->gc_sections) { + for (ii = 0; ii < l->ninputs; ++ii) { + ObjBuilder* ob = l->inputs[ii].obj; + u32 nsec = obj_section_count(ob); + for (j = 1; j < nsec; ++j) { + const Section* s = obj_section_get(ob, j); + if (s && section_kept(s)) g->marks[ii][j] = 1; + } + } + return; + } + + memset(&q, 0, sizeof(q)); + + /* Static roots: SF_RETAIN + init/fini/preinit_array. */ + for (ii = 0; ii < l->ninputs; ++ii) { + ObjBuilder* ob = l->inputs[ii].obj; + u32 nsec = obj_section_count(ob); + for (j = 1; j < nsec; ++j) { + const Section* s = obj_section_get(ob, j); + int root; + if (!s || !section_kept(s)) continue; + root = (s->flags & SF_RETAIN) + || s->sem == SSEM_INIT_ARRAY + || s->sem == SSEM_FINI_ARRAY + || s->sem == SSEM_PREINIT_ARRAY; + if (root) gc_mark(g, &q, h, ii, j); + } + } + + /* Entry symbol's defining section. Linker default entry is "_start" + * (set in link_new); the JIT path overrides via link_set_entry. */ + if (l->entry_name != 0) { + LinkSymId id = symhash_get(&img->globals, l->entry_name); + u32 tii; ObjSecId tsid; + if (gc_def_site(img, l, id, &tii, &tsid)) + gc_mark(g, &q, h, tii, tsid); + } + + /* Worklist: pop a live section, mark every section reachable through + * its relocs. Each reloc's target may also be a __start_/__stop_ + * encoding-section reference, in which case sections of that name + * become live. */ + while (q.n > 0) { + u64 v = q.items[--q.n]; + u32 cii = GC_II(v); + ObjSecId cj = GC_J(v); + ObjBuilder* ob = l->inputs[cii].obj; + InputMap* m = &img->input_maps[cii]; + u32 nsec = obj_section_count(ob); + u32 total = 0; + const Reloc* base; + for (k = 0; k < nsec; ++k) total += obj_reloc_count(ob, k); + if (!total) continue; + base = obj_relocs(ob, 0); + for (k = 0; k < total; ++k) { + const Reloc* r = &base[k]; + LinkSymId target; + const LinkSymbol* tsym; + u32 tii; ObjSecId tsid; + if (r->section_id != cj) continue; + if (r->sym == OBJ_SYM_NONE || r->sym >= m->nsym) continue; + target = m->sym[r->sym]; + if (target == LINK_SYM_NONE) continue; + tsym = &img->syms[target - 1]; + + if (tsym->name != 0) { + size_t namelen, off, ilen; + const char* nm = pool_str(l->c->global, tsym->name, &namelen); + if (gc_split_start_stop(nm, namelen, &off, &ilen, NULL)) { + Sym secname = pool_intern(l->c->global, nm + off, ilen); + gc_promote_by_section_name(l, g, &q, h, secname); + } + } + + if (gc_def_site(img, l, target, &tii, &tsid)) + gc_mark(g, &q, h, tii, tsid); + } + } + + if (q.items) h->free(h, q.items, sizeof(*q.items) * q.cap); +} + +/* After layout, clear `defined` on every LinkSymbol whose contributing + * input section was dropped. The global hash entry stays — lookups + * (cfree_jit_lookup, link_symbol_lookup) gate on `defined`. */ +static void gc_drop_dead_globals(Linker* l, LinkImage* img, const GcLive* g) +{ + u32 i; + if (!l->gc_sections) return; + for (i = 0; i < img->nsyms; ++i) { + LinkSymbol* s = &img->syms[i]; + ObjBuilder* ob; + const ObjSym* osym; + ObjSecId osid; + if (!s->defined) continue; + if (s->kind == SK_ABS || s->kind == SK_COMMON) continue; + if (s->input_id == LINK_INPUT_NONE) continue; + ob = l->inputs[s->input_id - 1].obj; + osym = obj_symbol_get(ob, s->obj_sym); + if (!osym) continue; + osid = osym->section_id; + if (osid == OBJ_SEC_NONE) continue; + if (gc_live_get(g, (u32)(s->input_id - 1u), osid)) continue; + /* Section was dropped — sym vanishes from the output. */ + s->defined = 0; + s->vaddr = 0; + s->section_id = LINK_SEC_NONE; + } +} + /* ---- pass 2: section assignment + segment layout ---- */ typedef struct SecRef { @@ -325,17 +630,17 @@ typedef struct SecRef { LinkSectionId link_sec_id; } SecRef; -static void layout_sections(Linker* l, LinkImage* img) +static void layout_sections(Linker* l, LinkImage* img, const GcLive* g) { Heap* h = img->heap; - /* First pass: count kept sections. */ + /* First pass: count kept sections (filtered by GC liveness). */ u32 ii, j; u32 total_kept = 0; for (ii = 0; ii < l->ninputs; ++ii) { ObjBuilder* ob = l->inputs[ii].obj; for (j = 1; j < obj_section_count(ob); ++j) { const Section* s = obj_section_get(ob, j); - if (s && section_kept(s)) ++total_kept; + if (s && section_kept(s) && gc_live_get(g, ii, j)) ++total_kept; } } @@ -367,7 +672,7 @@ static void layout_sections(Linker* l, LinkImage* img) LinkSection* ls; LinkSectionId lsid; - if (!s || !section_kept(s)) continue; + if (!s || !section_kept(s) || !gc_live_get(g, ii, j)) continue; bucket = bucket_for(s->flags); align = s->align ? s->align : 1u; @@ -736,6 +1041,57 @@ static void emit_array_boundaries(Linker* l, LinkImage* img) emit_boundary_sym(l, img, "__fini_array_end", fini_end); } +/* ---- pass 3c: __start_<X>/__stop_<X> encoding-section boundaries ---- + * + * For every undef LinkSymbol whose name is __start_<X>/__stop_<X> with + * <X> a valid C identifier, find the span of every output LinkSection + * sourced from an input section named <X>, and resolve the symbol to + * its low (start) or high (stop) vaddr. Sections that were dropped by + * GC don't contribute (m->section[j] == LINK_SEC_NONE). */ +static void emit_encoding_section_boundaries(Linker* l, LinkImage* img) +{ + u32 i, ii, j; + for (i = 0; i < img->nsyms; ++i) { + LinkSymbol* sym = &img->syms[i]; + const char* nm; + size_t namelen, off, ilen; + int is_start; + Sym secname; + u64 lo = (u64)-1; + u64 hi = 0; + int found = 0; + if (sym->defined) continue; + if (sym->name == 0) continue; + nm = pool_str(l->c->global, sym->name, &namelen); + if (!gc_split_start_stop(nm, namelen, &off, &ilen, &is_start)) continue; + secname = pool_intern(l->c->global, nm + off, ilen); + for (ii = 0; ii < l->ninputs; ++ii) { + ObjBuilder* ob = l->inputs[ii].obj; + InputMap* m = &img->input_maps[ii]; + for (j = 1; j < obj_section_count(ob); ++j) { + const Section* s = obj_section_get(ob, j); + LinkSectionId ls_id; + const LinkSection* ls; + u64 start, end; + if (!s || s->name != secname) continue; + ls_id = m->section[j]; + if (ls_id == LINK_SEC_NONE) continue; + ls = &img->sections[ls_id - 1]; + start = ls->vaddr; + end = ls->vaddr + ls->size; + if (start < lo) lo = start; + if (end > hi) hi = end; + found = 1; + } + } + if (!found) continue; + sym->kind = SK_OBJ; + sym->bind = SB_GLOBAL; + sym->defined = 1; + sym->vaddr = is_start ? lo : hi; + } +} + /* ---- pass 4: relocation records ---- */ static u8 reloc_width(RelocKind k) @@ -790,6 +1146,8 @@ static void emit_reloc_records(Linker* l, LinkImage* img, LinkSection* ls; LinkRelocApply rec; if (!s || !section_kept(s)) continue; + /* Skip relocs whose containing section was GC'd. */ + if (m->section[r->section_id] == LINK_SEC_NONE) continue; if (r->sym == OBJ_SYM_NONE || r->sym >= m->nsym) compiler_panic(l->c, no_loc(), "link: reloc references unknown symbol"); @@ -885,6 +1243,7 @@ static void layout_got(Linker* l, LinkImage* img, LinkSymId** got_map_out) const Section* s = obj_section_get(ob, r->section_id); LinkSymId target; if (!s || !section_kept(s)) continue; + if (m->section[r->section_id] == LINK_SEC_NONE) continue; if (!reloc_uses_got(r->kind)) continue; if (r->sym == OBJ_SYM_NONE || r->sym >= m->nsym) continue; target = m->sym[r->sym]; @@ -1210,21 +1569,29 @@ LinkImage* link_resolve(Linker* l) memset(img->input_maps, 0, sizeof(*img->input_maps) * l->ninputs); resolve_symbols(l, img); - layout_sections(l, img); - layout_commons(l, img); - emit_segment_bytes(l, img); - link_symbols_to_sections(l, img); - emit_array_boundaries(l, img); - resolve_undefs(l, img); { - LinkSymId* got_map = NULL; - u32 got_map_size = img->nsyms + 1u; - layout_got(l, img, &got_map); - emit_reloc_records(l, img, got_map); - if (got_map) - h->free(h, got_map, sizeof(*got_map) * got_map_size); + GcLive g = {0}; + gc_live_alloc(&g, l, h); + gc_compute(l, img, &g); + layout_sections(l, img, &g); + layout_commons(l, img); + emit_segment_bytes(l, img); + link_symbols_to_sections(l, img); + emit_array_boundaries(l, img); + emit_encoding_section_boundaries(l, img); + resolve_undefs(l, img); + gc_drop_dead_globals(l, img, &g); + { + LinkSymId* got_map = NULL; + u32 got_map_size = img->nsyms + 1u; + layout_got(l, img, &got_map); + emit_reloc_records(l, img, got_map); + if (got_map) + h->free(h, got_map, sizeof(*got_map) * got_map_size); + } + resolve_entry(l, img); + gc_live_free(&g, h); } - resolve_entry(l, img); return img; } diff --git a/src/obj/elf.h b/src/obj/elf.h @@ -86,6 +86,7 @@ #define SHF_LINK_ORDER 0x80u #define SHF_GROUP 0x200u #define SHF_TLS 0x400u +#define SHF_GNU_RETAIN 0x200000u /* ---- symbol bind / type / visibility ---- */ #define STB_LOCAL 0 diff --git a/src/obj/elf_emit.c b/src/obj/elf_emit.c @@ -77,6 +77,7 @@ static u32 sec_flags_to_elf(u16 flags) if (flags & SF_STRINGS) r |= SHF_STRINGS; if (flags & SF_GROUP) r |= SHF_GROUP; if (flags & SF_LINK_ORDER) r |= SHF_LINK_ORDER; + if (flags & SF_RETAIN) r |= SHF_GNU_RETAIN; return (u32)r; } @@ -633,13 +634,14 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) * GNU ld both emit it for Linux targets. Linking does not key off * EI_OSABI for plain AArch64 ELF — it's e_machine that matters. * - * Exception: STT_GNU_IFUNC is a GNU extension and the ELF spec - * requires EI_OSABI=ELFOSABI_GNU when the file uses any GNU symbol - * extensions. Clang emits ELFOSABI_GNU for IFUNC-using TUs. */ + * Exception: GNU extensions (STT_GNU_IFUNC, SHF_GNU_RETAIN, ...) + * require EI_OSABI=ELFOSABI_GNU. Clang sets it for any TU using a + * GNU-flavored marker; we mirror that so roundtrip is byte-stable. */ ident[EI_OSABI] = ELFOSABI_NONE; { ObjSymIter* it = obj_symiter_new(ob); ObjSymEntry e; + u32 nsec = obj_section_count(ob), si; while (obj_symiter_next(it, &e)) { if (e.sym->kind == SK_IFUNC) { ident[EI_OSABI] = ELFOSABI_GNU; @@ -647,6 +649,15 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) } } obj_symiter_free(it); + if (ident[EI_OSABI] != ELFOSABI_GNU) { + for (si = 1; si < nsec; ++si) { + const Section* sec = obj_section_get(ob, si); + if (sec && (sec->flags & SF_RETAIN)) { + ident[EI_OSABI] = ELFOSABI_GNU; + break; + } + } + } } cfree_writer_seek(w, 0); cfree_writer_write(w, ident, EI_NIDENT); diff --git a/src/obj/elf_read.c b/src/obj/elf_read.c @@ -55,7 +55,8 @@ static void parse_shdr(const u8* p, ShdrRec* out) * compressed .debug_*, SHF_INFO_LINK (0x40) on .rela.* sections. */ #define ELF_KNOWN_FLAGS_MASK \ ((u64)(SHF_ALLOC | SHF_EXECINSTR | SHF_WRITE | SHF_TLS | \ - SHF_MERGE | SHF_STRINGS | SHF_GROUP | SHF_LINK_ORDER)) + SHF_MERGE | SHF_STRINGS | SHF_GROUP | SHF_LINK_ORDER | \ + SHF_GNU_RETAIN)) static u16 elf_flags_to_obj(u64 f) { @@ -68,6 +69,7 @@ static u16 elf_flags_to_obj(u64 f) if (f & SHF_STRINGS) r |= SF_STRINGS; if (f & SHF_GROUP) r |= SF_GROUP; if (f & SHF_LINK_ORDER) r |= SF_LINK_ORDER; + if (f & SHF_GNU_RETAIN) r |= SF_RETAIN; return r; } diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -22,6 +22,7 @@ typedef enum SecFlag { SF_STRINGS = 1u << 5, SF_GROUP = 1u << 6, SF_LINK_ORDER= 1u << 7, + SF_RETAIN = 1u << 8, /* SHF_GNU_RETAIN: do not GC even if unreferenced */ } SecFlag; typedef enum SecSem { diff --git a/test/link/CORPUS.md b/test/link/CORPUS.md @@ -35,7 +35,9 @@ expects from the combined sequence. | `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 | -| `gc_absent` | one symbol per line that must be absent post --gc-sections | +| `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) | Negative tests live in `test/link/bad/<name>/` instead of `cases/`. Each @@ -91,7 +93,14 @@ Cases 02–09 all pair ADR_PREL_PG_HI21 with their primary LDST reloc. | # | 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 | +| 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 | diff --git a/test/link/cases/25_gc_sections/a.c b/test/link/cases/25_gc_sections/a.c @@ -1,8 +0,0 @@ -/* --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/25a_gc_basic/a.c b/test/link/cases/25a_gc_basic/a.c @@ -0,0 +1,8 @@ +/* --gc-sections at function granularity: with -ffunction-sections each + * fn lands in .text.<sym>, so an unreachable function gets dropped on + * its own. Verifies the basic transitive-from-entry liveness rule. */ + +__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/25a_gc_basic/cflags b/test/link/cases/25a_gc_basic/cflags @@ -0,0 +1 @@ +-ffunction-sections diff --git a/test/link/cases/25_gc_sections/gc_absent b/test/link/cases/25a_gc_basic/gc_absent diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25a_gc_basic/linker_flags diff --git a/test/link/cases/25b_no_gc_baseline/a.c b/test/link/cases/25b_no_gc_baseline/a.c @@ -0,0 +1,7 @@ +/* Baseline for case 25a: identical source, no --gc-sections. The + * unreachable function survives because GC is off. */ + +__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/25b_no_gc_baseline/cflags b/test/link/cases/25b_no_gc_baseline/cflags @@ -0,0 +1 @@ +-ffunction-sections diff --git a/test/link/cases/25_gc_sections/gc_absent b/test/link/cases/25b_no_gc_baseline/gc_present diff --git a/test/link/cases/25c_gc_data/a.c b/test/link/cases/25c_gc_data/a.c @@ -0,0 +1,7 @@ +/* --gc-sections at data granularity: with -fdata-sections each global + * lands in .data.<sym>/.rodata.<sym>/.bss.<sym>; unreferenced ones get + * dropped. */ + +int unused_global = 42; + +int test_main(void) { return 0; } diff --git a/test/link/cases/25c_gc_data/cflags b/test/link/cases/25c_gc_data/cflags @@ -0,0 +1 @@ +-fdata-sections -ffunction-sections diff --git a/test/link/cases/25c_gc_data/gc_absent b/test/link/cases/25c_gc_data/gc_absent @@ -0,0 +1 @@ +unused_global diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25c_gc_data/linker_flags diff --git a/test/link/cases/25d_gc_chain/a.c b/test/link/cases/25d_gc_chain/a.c @@ -0,0 +1,19 @@ +/* Transitive closure: test_main calls live_chain_a → live_chain_b → + * live_chain_c. The dead_chain_* triple has no caller — all three + * should be dropped together. + * + * volatile sink writes are required to stop clang from constant-folding + * trivial-return chains away (which would erase the very relocs the GC + * pass needs to walk). */ + +volatile int sink; + +__attribute__((noinline)) int live_chain_c(void) { sink |= 0xC; return 0; } +__attribute__((noinline)) int live_chain_b(void) { sink |= 0xB; return live_chain_c(); } +__attribute__((noinline)) int live_chain_a(void) { sink |= 0xA; return live_chain_b(); } + +__attribute__((noinline)) int dead_chain_c(void) { sink |= 0x0C; return 0; } +__attribute__((noinline)) int dead_chain_b(void) { sink |= 0x0B; return dead_chain_c(); } +__attribute__((noinline)) int dead_chain_a(void) { sink |= 0x0A; return dead_chain_b(); } + +int test_main(void) { return live_chain_a(); } diff --git a/test/link/cases/25d_gc_chain/cflags b/test/link/cases/25d_gc_chain/cflags @@ -0,0 +1 @@ +-ffunction-sections diff --git a/test/link/cases/25d_gc_chain/gc_absent b/test/link/cases/25d_gc_chain/gc_absent @@ -0,0 +1,3 @@ +dead_chain_a +dead_chain_b +dead_chain_c diff --git a/test/link/cases/25d_gc_chain/gc_present b/test/link/cases/25d_gc_chain/gc_present @@ -0,0 +1,3 @@ +live_chain_a +live_chain_b +live_chain_c diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25d_gc_chain/linker_flags diff --git a/test/link/cases/25e_gc_keep_init_array/a.c b/test/link/cases/25e_gc_keep_init_array/a.c @@ -0,0 +1,9 @@ +/* GC root: .init_array. The constructor has no static caller — only + * the init_array entry references it — but it must survive --gc-sections + * and run before test_main. */ + +static int g_flag; + +__attribute__((constructor)) static void set_flag(void) { g_flag = 7; } + +int test_main(void) { return g_flag == 7 ? 0 : 1; } diff --git a/test/link/cases/25e_gc_keep_init_array/cflags b/test/link/cases/25e_gc_keep_init_array/cflags @@ -0,0 +1 @@ +-ffunction-sections -fdata-sections diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25e_gc_keep_init_array/linker_flags diff --git a/test/link/cases/25f_gc_retain/a.c b/test/link/cases/25f_gc_retain/a.c @@ -0,0 +1,7 @@ +/* GC root: SHF_GNU_RETAIN. The retain attribute marks the section + * containing kept_helper as exempt from --gc-sections, even though + * nothing in the live closure references it. */ + +__attribute__((retain, used)) int kept_helper(void) { return 99; } + +int test_main(void) { return 0; } diff --git a/test/link/cases/25f_gc_retain/cflags b/test/link/cases/25f_gc_retain/cflags @@ -0,0 +1 @@ +-ffunction-sections diff --git a/test/link/cases/25f_gc_retain/gc_present b/test/link/cases/25f_gc_retain/gc_present @@ -0,0 +1 @@ +kept_helper diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25f_gc_retain/linker_flags diff --git a/test/link/cases/25g_gc_keeps_data_via_reloc/a.c b/test/link/cases/25g_gc_keeps_data_via_reloc/a.c @@ -0,0 +1,7 @@ +/* Reloc-following: a live function references a global; the data + * section holding that global must survive even with -fdata-sections + * (which puts each global in its own droppable section). */ + +int data_val = 42; + +int test_main(void) { return data_val == 42 ? 0 : 1; } diff --git a/test/link/cases/25g_gc_keeps_data_via_reloc/cflags b/test/link/cases/25g_gc_keeps_data_via_reloc/cflags @@ -0,0 +1 @@ +-ffunction-sections -fdata-sections diff --git a/test/link/cases/25g_gc_keeps_data_via_reloc/gc_present b/test/link/cases/25g_gc_keeps_data_via_reloc/gc_present @@ -0,0 +1 @@ +data_val diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25g_gc_keeps_data_via_reloc/linker_flags diff --git a/test/link/cases/25h_gc_start_stop/a.c b/test/link/cases/25h_gc_start_stop/a.c @@ -0,0 +1,20 @@ +/* __start_<X> / __stop_<X> retention. Items live in section "custom"; + * the test sums them by walking [__start_custom, __stop_custom). The + * linker must (a) synthesize the boundary symbols at the section span, + * (b) keep the section alive under --gc-sections because of those refs. */ + +/* No `retain` here: GC retention must come from __start_/__stop_, + * not SHF_GNU_RETAIN. `used` only stops clang from dropping the + * symbol pre-link. */ +__attribute__((section("custom"), used)) int item_a = 1; +__attribute__((section("custom"), used)) int item_b = 2; + +extern int __start_custom[]; +extern int __stop_custom[]; + +int test_main(void) +{ + int sum = 0; + for (int* p = __start_custom; p != __stop_custom; ++p) sum += *p; + return sum == 3 ? 0 : 1; +} diff --git a/test/link/cases/25_gc_sections/linker_flags b/test/link/cases/25h_gc_start_stop/linker_flags diff --git a/test/link/harness/jit_runner.c b/test/link/harness/jit_runner.c @@ -2,6 +2,7 @@ * * Usage: * jit_runner [--gc-sections] [--use-resolver] + * [--check-absent SYM] [--check-present SYM] * [--archive [--whole-archive] <lib.a>] <in.o> ... * * Reads .o (and optionally .a) inputs, calls cfree_link_jit (which runs @@ -195,8 +196,10 @@ int main(int argc, char** argv) 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; + /* --check-absent SYM: after link, verify symbol is not in image. + * --check-present SYM: after link, verify symbol IS in image. */ + const char* check_absent = NULL; + const char* check_present = NULL; CfreeBytesInput objs[64]; CfreeBytesInputArchive archives[16]; @@ -210,6 +213,7 @@ int main(int argc, char** argv) 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 if (!strcmp(argv[i], "--check-present") && i+1 < argc) { check_present = argv[++i]; } else { uint8_t* data; size_t len; if (slurp(argv[i], &data, &len)) { @@ -287,6 +291,18 @@ int main(int argc, char** argv) return absent_ok ? 0 : 1; } + /* --check-present SYM: verify symbol survives the link / GC. */ + if (check_present) { + int present_ok = cfree_jit_lookup(jit, check_present) != NULL; + if (!present_ok) + fprintf(stderr, "jit-runner: symbol '%s' missing but expected present\n", + check_present); + cfree_jit_run_dtors(jit); + cfree_jit_free(jit); + cfree_compiler_free(c); + return present_ok ? 0 : 1; + } + int (*fn)(void) = cfree_jit_lookup(jit, "test_main"); int result = fn ? fn() : 1; diff --git a/test/link/run.sh b/test/link/run.sh @@ -23,8 +23,11 @@ # jit_only — skip R and E paths; run J only # 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) +# cflags — extra clang -c flags appended to every TU compile +# gc_absent — one symbol per line; verified absent post-link +# (jit_runner --check-absent for J; readelf -s for E) +# gc_present — one symbol per line; verified present post-link +# (jit_runner --check-present for J; readelf -s for E) # archive_b — package b.o into b.a; content "demand" or "whole" # # Filtering: @@ -191,13 +194,30 @@ for case_dir in "$TEST_DIR/cases"/*/; do done < "$case_dir/linker_flags" fi - # Collect GC-absent symbols + # Collect GC-absent / GC-present 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 + gc_present_syms=() + if [ -f "$case_dir/gc_present" ]; then + while IFS= read -r sym; do + [ -n "$sym" ] && gc_present_syms+=("$sym") + done < "$case_dir/gc_present" + fi + + # Per-case extra clang cflags (mirrors test/elf/cases/*.cflags but as a + # file per case dir). One token per whitespace. + case_cflags=() + if [ -f "$case_dir/cflags" ]; then + while IFS= read -r line; do + for tok in $line; do + [ -n "$tok" ] && case_cflags+=("$tok") + done + done < "$case_dir/cflags" + fi # Collect source files tu_srcs=() @@ -219,6 +239,7 @@ for case_dir in "$TEST_DIR/cases"/*/; do obj="$work/${base}.o" if ! clang $CLANG_TARGET -O1 -fno-inline -ffreestanding -fno-stack-protector \ -fno-PIC -fno-pie -fcommon \ + "${case_cflags[@]}" \ -c "$src" -o "$obj" 2>"$work/compile_${base}.err"; then compile_ok=0; break fi @@ -304,8 +325,45 @@ for case_dir in "$TEST_DIR/cases"/*/; do elif [ $have_runner -eq 1 ]; then run_aarch64 "$exe" "$work/exec.out" "$work/exec.err" dt=$(( $(now_ms) - t0 )); T_E=$(( T_E + dt )) - if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (${dt}ms)" - else note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"; fi + + # Symbol presence/absence checks via readelf -s. The cfree + # exe writer emits PHDRs only — no .symtab — so on the E path + # this check is skipped today. (The J path validates presence + # via cfree_jit_lookup, which is the authoritative check.) We + # keep the wiring here so the day cfree-link-exe gains a symtab + # the verification activates without further harness changes. + e_ok=1 + if [ "$RUN_RC" -ne "$expected" ]; then e_ok=0; fi + if [ $e_ok -eq 1 ] && [ $have_readelf -eq 1 ] && \ + { [ ${#gc_absent_syms[@]} -gt 0 ] || \ + [ ${#gc_present_syms[@]} -gt 0 ]; }; then + "$READELF_BIN" -sW "$exe" >"$work/exec_syms.txt" 2>/dev/null + if [ -s "$work/exec_syms.txt" ]; then + for sym in "${gc_absent_syms[@]}"; do + if awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ + "$work/exec_syms.txt"; then + e_ok=0 + note_fail "$name/E gc_absent: '$sym' present" + break + fi + done + if [ $e_ok -eq 1 ]; then + for sym in "${gc_present_syms[@]}"; do + if ! awk -v s="$sym" 'NR>2 && $NF==s && $7!="UND" {found=1} END{exit !found}' \ + "$work/exec_syms.txt"; then + e_ok=0 + note_fail "$name/E gc_present: '$sym' missing" + break + fi + done + fi + fi + fi + + if [ $e_ok -eq 1 ]; then + if [ "$RUN_RC" -eq "$expected" ]; then note_pass "$name/E (${dt}ms)" + else note_fail "$name/E (expected $expected, got $RUN_RC, ${dt}ms)"; fi + fi else note_skip "$name/E" "no runner (qemu/podman)" fi @@ -325,12 +383,9 @@ for case_dir in "$TEST_DIR/cases"/*/; do "${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 gc_sections cases, additionally verify the absent and + # present symbol sets via the jit_runner's --check-* flags. + # Re-run jit_runner once per symbol; first failure wins. for sym in "${gc_absent_syms[@]}"; do if "${jit_cmd[@]}" --check-absent "$sym" \ >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then @@ -340,6 +395,17 @@ for case_dir in "$TEST_DIR/cases"/*/; do break fi done + if [ "$j_rc" -eq "$expected" ]; then + for sym in "${gc_present_syms[@]}"; do + if "${jit_cmd[@]}" --check-present "$sym" \ + >"$work/jit_gc.out" 2>"$work/jit_gc.err"; then + : # present check passed + else + j_rc=$? + break + fi + done + fi dt=$(( $(now_ms) - t0 )); T_J=$(( T_J + dt )) if [ "$j_rc" -eq "$expected" ]; then note_pass "$name/J (${dt}ms)"