commit 52657d87b5877bbb89423ba0b30d8fda0b154bbf
parent 13c7eb6490e2aaef2c457641287d02eadde21ed2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 9 May 2026 04:15:49 -0700
obj: add ELF read/emit and object file support with ELF tests
Diffstat:
23 files changed, 3026 insertions(+), 0 deletions(-)
diff --git a/src/obj/elf.h b/src/obj/elf.h
@@ -0,0 +1,183 @@
+/* ELF wire-format constants, structs, and small helpers shared between
+ * obj/elf_emit.c, obj/elf_read.c, and link/elf_exe.c.
+ *
+ * Private to src/. The public ObjBuilder/Linker surface is format-neutral
+ * (obj/obj.h, link/link.h); the ELF spelling of those abstractions only
+ * exists inside libcfree.
+ *
+ * Scope: 64-bit little-endian only. AArch64 today; the per-arch reloc
+ * mapping in elf_aarch64_reloc_{to,from} is the place to extend when
+ * x86_64/RISC-V/etc. land. */
+
+#ifndef CFREE_OBJ_ELF_H
+#define CFREE_OBJ_ELF_H
+
+#include <cfree.h>
+
+#include "core/core.h"
+#include "obj/obj.h"
+
+/* ---- e_ident indices and values ---- */
+#define EI_NIDENT 16
+#define EI_MAG0 0
+#define EI_MAG1 1
+#define EI_MAG2 2
+#define EI_MAG3 3
+#define EI_CLASS 4
+#define EI_DATA 5
+#define EI_VERSION 6
+#define EI_OSABI 7
+
+#define ELFMAG0 0x7f
+#define ELFMAG1 'E'
+#define ELFMAG2 'L'
+#define ELFMAG3 'F'
+#define ELFCLASS64 2
+#define ELFDATA2LSB 1
+#define EV_CURRENT 1
+#define ELFOSABI_NONE 0
+#define ELFOSABI_LINUX 3
+
+/* ---- e_type ---- */
+#define ET_NONE 0
+#define ET_REL 1
+#define ET_EXEC 2
+#define ET_DYN 3
+
+/* ---- e_machine ---- */
+#define EM_X86_64 0x3E
+#define EM_AARCH64 0xB7
+
+/* ---- header sizes (also literal e_*size fields) ---- */
+#define ELF64_EHDR_SIZE 64
+#define ELF64_SHDR_SIZE 64
+#define ELF64_PHDR_SIZE 56
+#define ELF64_SYM_SIZE 24
+#define ELF64_RELA_SIZE 24
+
+/* ---- special section indices ---- */
+#define SHN_UNDEF 0u
+#define SHN_ABS 0xfff1u
+#define SHN_COMMON 0xfff2u
+
+/* ---- sh_type ---- */
+#define SHT_NULL 0
+#define SHT_PROGBITS 1
+#define SHT_SYMTAB 2
+#define SHT_STRTAB 3
+#define SHT_RELA 4
+#define SHT_NOTE 7
+#define SHT_NOBITS 8
+#define SHT_REL 9
+#define SHT_INIT_ARRAY 14
+#define SHT_FINI_ARRAY 15
+#define SHT_PREINIT_ARRAY 16
+#define SHT_GROUP 17
+
+/* ---- sh_flags ---- */
+#define SHF_WRITE 0x1u
+#define SHF_ALLOC 0x2u
+#define SHF_EXECINSTR 0x4u
+#define SHF_MERGE 0x10u
+#define SHF_STRINGS 0x20u
+#define SHF_INFO_LINK 0x40u
+#define SHF_LINK_ORDER 0x80u
+#define SHF_GROUP 0x200u
+#define SHF_TLS 0x400u
+
+/* ---- symbol bind / type / visibility ---- */
+#define STB_LOCAL 0
+#define STB_GLOBAL 1
+#define STB_WEAK 2
+
+#define STT_NOTYPE 0
+#define STT_OBJECT 1
+#define STT_FUNC 2
+#define STT_SECTION 3
+#define STT_FILE 4
+#define STT_COMMON 5
+#define STT_TLS 6
+
+#define STV_DEFAULT 0
+#define STV_INTERNAL 1
+#define STV_HIDDEN 2
+#define STV_PROTECTED 3
+
+#define ELF64_ST_INFO(b,t) ((u8)((((u32)(b)) << 4) | ((u32)(t) & 0xf)))
+#define ELF64_ST_BIND(i) ((u32)((i) >> 4))
+#define ELF64_ST_TYPE(i) ((u32)((i) & 0xf))
+#define ELF64_R_SYM(i) ((u32)((i) >> 32))
+#define ELF64_R_TYPE(i) ((u32)((i) & 0xffffffffu))
+#define ELF64_R_INFO(s,t) ((((u64)(s)) << 32) | ((u64)(t) & 0xffffffffull))
+
+/* ---- program header ---- */
+#define PT_LOAD 1
+#define PF_X 0x1u
+#define PF_W 0x2u
+#define PF_R 0x4u
+
+/* ---- AArch64 ELF wire-format relocation type codes ----
+ * Prefixed ELF_ to avoid collision with the cfree-canonical RelocKind
+ * enum values in obj.h (R_AARCH64_*). */
+#define ELF_R_AARCH64_NONE 0
+#define ELF_R_AARCH64_ABS64 257
+#define ELF_R_AARCH64_ABS32 258
+#define ELF_R_AARCH64_PREL64 260
+#define ELF_R_AARCH64_PREL32 261
+#define ELF_R_AARCH64_ADR_PREL_PG_HI21 275
+#define ELF_R_AARCH64_ADD_ABS_LO12_NC 277
+#define ELF_R_AARCH64_LDST8_ABS_LO12_NC 278
+#define ELF_R_AARCH64_CALL26 283
+#define ELF_R_AARCH64_LDST16_ABS_LO12_NC 284
+#define ELF_R_AARCH64_LDST32_ABS_LO12_NC 285
+#define ELF_R_AARCH64_LDST64_ABS_LO12_NC 286
+#define ELF_R_AARCH64_LDST128_ABS_LO12_NC 299
+
+/* Map cfree-canonical RelocKind <-> AArch64 ELF reloc type. Returns
+ * R_AARCH64_NONE (0) on unsupported kinds; emit_elf treats that as a
+ * fatal error.
+ *
+ * Note: R_REL32/R_REL64 collapse to R_AARCH64_PREL32/64 on AArch64.
+ * cfree's distinction between "section-relative" and "PC-relative" is
+ * not currently exercised on AArch64 (the linker resolves to absolute
+ * vaddrs); document any divergence in the per-arch tables when other
+ * archs land. */
+u32 elf_aarch64_reloc_to (u32 kind /* RelocKind */);
+u32 elf_aarch64_reloc_from(u32 elf_type);
+
+/* ---- little-endian byte writers/readers ---- */
+static inline void elf_wr_u8 (Writer* w, u32 v)
+{ u8 b = (u8)v; cfree_writer_write(w, &b, 1); }
+
+static inline void elf_wr_u16(Writer* w, u32 v)
+{
+ u8 b[2] = { (u8)(v), (u8)(v >> 8) };
+ cfree_writer_write(w, b, 2);
+}
+
+static inline void elf_wr_u32(Writer* w, u32 v)
+{
+ u8 b[4] = { (u8)(v), (u8)(v >> 8), (u8)(v >> 16), (u8)(v >> 24) };
+ cfree_writer_write(w, b, 4);
+}
+
+static inline void elf_wr_u64(Writer* w, u64 v)
+{
+ u8 b[8] = {
+ (u8)(v), (u8)(v >> 8), (u8)(v >> 16), (u8)(v >> 24),
+ (u8)(v >> 32), (u8)(v >> 40), (u8)(v >> 48), (u8)(v >> 56),
+ };
+ cfree_writer_write(w, b, 8);
+}
+
+static inline u16 elf_rd_u16(const u8* p) { return (u16)(p[0] | (p[1] << 8)); }
+static inline u32 elf_rd_u32(const u8* p)
+{
+ return (u32)p[0] | ((u32)p[1] << 8) | ((u32)p[2] << 16) | ((u32)p[3] << 24);
+}
+static inline u64 elf_rd_u64(const u8* p)
+{
+ return (u64)elf_rd_u32(p) | ((u64)elf_rd_u32(p + 4) << 32);
+}
+
+#endif /* CFREE_OBJ_ELF_H */
diff --git a/src/obj/elf_emit.c b/src/obj/elf_emit.c
@@ -0,0 +1,613 @@
+/* ELF ET_REL writer. Walks a finalized ObjBuilder and emits a 64-bit
+ * little-endian relocatable object via the supplied Writer.
+ *
+ * Layout strategy:
+ * 1. plan ELF section headers (one per obj section, plus synthesized
+ * .symtab / .strtab / .shstrtab and one .rela.<name> per obj section
+ * that carries relocations);
+ * 2. build .symtab + .strtab content (locals first — STT_SECTION
+ * synthesized for every input section, then ordinary locals, then
+ * globals/weaks);
+ * 3. build .rela.* content using the AArch64 reloc map;
+ * 4. build .shstrtab;
+ * 5. assign file offsets sequentially, respecting per-section
+ * addralign;
+ * 6. write Ehdr, then each section's bytes (seeking to its sh_offset),
+ * then the section header table.
+ *
+ * AArch64 little-endian only. Other archs / endianness panic at entry —
+ * the per-arch reloc table is the place to extend, not this file.
+ *
+ * See doc/DESIGN.md §5.5 for the round-trip invariant: read_elf of this
+ * output must produce an ObjBuilder shape-equivalent to the input,
+ * modulo (a) section ordering and (b) the synthesized STT_SECTION
+ * symbols (which are visible to read_elf but were not in the input). */
+
+#include "obj/elf.h"
+
+#include "core/arena.h"
+#include "core/buf.h"
+#include "core/heap.h"
+#include "core/pool.h"
+
+#include <string.h>
+
+static SrcLoc no_loc(void) { SrcLoc l = {0,0,0}; return l; }
+
+/* ---- per-ELF-section plan record ---- */
+
+/* Internal section descriptor used during planning. Mirrors Elf64_Shdr
+ * but with an explicit pointer to the source bytes (either an obj
+ * Section's chunked Buf or a synthesized linear buffer). NOBITS sections
+ * have no source bytes and consume no file space. */
+typedef struct ElfSec {
+ /* Final shdr fields (little-endian-encoded at write time). */
+ u32 sh_name; /* offset into shstrtab */
+ u32 sh_type;
+ u64 sh_flags;
+ u64 sh_addr; /* always 0 for ET_REL */
+ u64 sh_offset;
+ u64 sh_size;
+ u32 sh_link;
+ u32 sh_info;
+ u64 sh_addralign;
+ u64 sh_entsize;
+
+ /* Section name. The name string lives in scratch (synthesized) or in
+ * the global pool (obj-section names); buf-source is set for sections
+ * carrying obj-section bytes, raw_bytes for synthesized. */
+ const char* name;
+ u32 name_len;
+
+ const Buf* obj_bytes; /* one of these three is set: */
+ const u8* raw_bytes; /* */
+ int is_nobits; /* */
+} ElfSec;
+
+/* ---- emit ---- */
+
+static u32 sec_flags_to_elf(u16 flags)
+{
+ u64 r = 0;
+ if (flags & SF_ALLOC) r |= SHF_ALLOC;
+ if (flags & SF_EXEC) r |= SHF_EXECINSTR;
+ if (flags & SF_WRITE) r |= SHF_WRITE;
+ if (flags & SF_TLS) r |= SHF_TLS;
+ if (flags & SF_MERGE) r |= SHF_MERGE;
+ if (flags & SF_STRINGS) r |= SHF_STRINGS;
+ if (flags & SF_GROUP) r |= SHF_GROUP;
+ if (flags & SF_LINK_ORDER) r |= SHF_LINK_ORDER;
+ return (u32)r;
+}
+
+static u32 sec_sem_to_elf(u16 sem)
+{
+ switch (sem) {
+ case SSEM_PROGBITS: return SHT_PROGBITS;
+ case SSEM_NOBITS: return SHT_NOBITS;
+ case SSEM_SYMTAB: return SHT_SYMTAB;
+ case SSEM_STRTAB: return SHT_STRTAB;
+ case SSEM_RELA: return SHT_RELA;
+ case SSEM_REL: return SHT_REL;
+ case SSEM_NOTE: return SHT_NOTE;
+ case SSEM_INIT_ARRAY: return SHT_INIT_ARRAY;
+ case SSEM_FINI_ARRAY: return SHT_FINI_ARRAY;
+ case SSEM_PREINIT_ARRAY: return SHT_PREINIT_ARRAY;
+ case SSEM_GROUP: return SHT_GROUP;
+ default: return SHT_PROGBITS;
+ }
+}
+
+static u8 sym_bind_to_elf(u16 bind)
+{
+ switch (bind) {
+ case SB_LOCAL: return STB_LOCAL;
+ case SB_GLOBAL: return STB_GLOBAL;
+ case SB_WEAK: return STB_WEAK;
+ default: return STB_LOCAL;
+ }
+}
+
+static u8 sym_kind_to_elf(u16 kind)
+{
+ switch (kind) {
+ case SK_UNDEF: return STT_NOTYPE;
+ case SK_FUNC: return STT_FUNC;
+ case SK_OBJ: return STT_OBJECT;
+ case SK_SECTION: return STT_SECTION;
+ case SK_FILE: return STT_FILE;
+ case SK_COMMON: return STT_COMMON;
+ case SK_TLS: return STT_TLS;
+ case SK_ABS: return STT_NOTYPE; /* SHN_ABS, NOTYPE */
+ case SK_NOTYPE: return STT_NOTYPE;
+ default: return STT_NOTYPE;
+ }
+}
+
+static u8 sym_vis_to_elf(u8 vis)
+{
+ switch (vis) {
+ case SV_DEFAULT: return STV_DEFAULT;
+ case SV_HIDDEN: return STV_HIDDEN;
+ case SV_PROTECTED: return STV_PROTECTED;
+ case SV_INTERNAL: return STV_INTERNAL;
+ default: return STV_DEFAULT;
+ }
+}
+
+static u16 sym_shndx(const ObjSym* s, const u32* obj_to_elf, u32 nsec)
+{
+ if (s->kind == SK_COMMON) return (u16)SHN_COMMON;
+ if (s->kind == SK_ABS) return (u16)SHN_ABS;
+ /* STT_FILE conventionally carries SHN_ABS as its shndx — its value
+ * field is not an address. Match clang/binutils. */
+ if (s->kind == SK_FILE) return (u16)SHN_ABS;
+ if (s->section_id == OBJ_SEC_NONE) return (u16)SHN_UNDEF;
+ if (s->section_id >= nsec) return (u16)SHN_UNDEF;
+ return (u16)obj_to_elf[s->section_id];
+}
+
+static const char* sym_to_str(Compiler* c, Sym n, u32* len_out)
+{
+ size_t len;
+ const char* s = pool_str(c->global, n, &len);
+ if (!s) { *len_out = 0; return ""; }
+ *len_out = (u32)len;
+ return s;
+}
+
+/* Append `len` bytes of `s` followed by a single NUL to `b`, return
+ * the offset at which `s` was placed.
+ *
+ * If `s` already exists at some offset (as a NUL-terminated substring
+ * starting at any offset), reuse that offset — clang/binutils both
+ * dedupe trivially identical strings, and matching the convention
+ * keeps our strtab the same size as theirs. The dedupe is linear in
+ * the strtab; section + symbol counts are small enough that this is
+ * fine without a hash. */
+static u32 strtab_add(Buf* b, const char* s, u32 len)
+{
+ /* Empty string: always at offset 0 (the leading NUL). */
+ if (len == 0) return 0;
+
+ /* Linear search for an existing copy. We must scan chunk-by-chunk
+ * because Buf is segmented; flatten to a temp scratch buffer first
+ * if non-empty and search there. For our tiny strtabs, the cost is
+ * dominated by the writes anyway. */
+ u32 total = buf_pos(b);
+ if (total > len) {
+ /* Flatten just to search — not optimal but the strtab here is
+ * always small (low kilobytes at most). */
+ u8 stack[256];
+ u8* tmp = total <= sizeof stack
+ ? stack
+ : (u8*)b->heap->alloc(b->heap, total, 1);
+ if (tmp) {
+ buf_flatten(b, tmp);
+ for (u32 i = 0; i + len < total; ++i) {
+ if (tmp[i + len] == 0 && memcmp(tmp + i, s, len) == 0) {
+ if (tmp != stack) b->heap->free(b->heap, tmp, total);
+ return i;
+ }
+ }
+ if (tmp != stack) b->heap->free(b->heap, tmp, total);
+ }
+ }
+
+ u32 off = total;
+ buf_write(b, s, len);
+ {
+ u8 z = 0;
+ buf_write(b, &z, 1);
+ }
+ return off;
+}
+
+static u32 align_up(u32 x, u32 a)
+{
+ if (a < 2) return x;
+ return (x + (a - 1)) & ~(a - 1);
+}
+
+static u64 align_up64(u64 x, u64 a)
+{
+ if (a < 2) return x;
+ return (x + (a - 1)) & ~(a - 1);
+}
+
+void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w)
+{
+ Heap* h = (Heap*)c->env->heap;
+
+ /* ---- target validation ------------------------------------------ */
+ if (c->target.arch != CFREE_ARCH_ARM_64) {
+ compiler_panic(c, no_loc(),
+ "emit_elf: only AArch64 is implemented (target arch=%u)",
+ (u32)c->target.arch);
+ }
+ if (c->target.big_endian) {
+ compiler_panic(c, no_loc(),
+ "emit_elf: big-endian AArch64 not supported");
+ }
+ if (c->target.ptr_size != 8) {
+ compiler_panic(c, no_loc(),
+ "emit_elf: ptr_size %u (expected 8)",
+ (u32)c->target.ptr_size);
+ }
+
+ /* ---- pass 1: plan ELF section list ------------------------------ */
+
+ u32 nobjsec = obj_section_count(ob);
+
+ /* Upper bound on ELF section count:
+ * 1 (SHN_UNDEF)
+ * + nobjsec - 1 (one ELF entry per real obj section)
+ * + nobjsec - 1 (worst case: a .rela.<name> per obj section)
+ * + 3 (.symtab, .strtab, .shstrtab)
+ */
+ u32 max_secs = 1 + (nobjsec - 1) + (nobjsec - 1) + 3;
+ if (max_secs < 4) max_secs = 4;
+ ElfSec* secs = arena_array(c->scratch, ElfSec, max_secs);
+ u32 nsecs = 0;
+ memset(&secs[nsecs++], 0, sizeof secs[0]); /* index 0 = SHN_UNDEF */
+
+ /* Map obj section id -> ELF section index. */
+ u32* obj_to_elf = arena_array(c->scratch, u32, nobjsec);
+ memset(obj_to_elf, 0, sizeof(u32) * nobjsec);
+
+ for (u32 i = 1; i < nobjsec; ++i) {
+ const Section* s = obj_section_get(ob, i);
+ ElfSec* es = &secs[nsecs];
+ memset(es, 0, sizeof *es);
+ u32 nlen;
+ es->name = sym_to_str(c, s->name, &nlen);
+ es->name_len = nlen;
+ es->sh_type = sec_sem_to_elf(s->sem);
+ es->sh_flags = sec_flags_to_elf(s->flags);
+ es->sh_addr = 0;
+ es->sh_addralign = s->align ? s->align : 1;
+ es->sh_entsize = s->entsize;
+ es->sh_link = 0;
+ es->sh_info = 0;
+ if (s->sem == SSEM_NOBITS) {
+ es->is_nobits = 1;
+ es->sh_size = s->bss_size;
+ } else {
+ es->obj_bytes = &s->bytes;
+ es->sh_size = s->bytes.total;
+ }
+ obj_to_elf[i] = nsecs++;
+ }
+
+ /* ---- pass 2: build .symtab + .strtab content -------------------- */
+
+ /* .strtab: leading NUL byte. Then a name per emitted symbol. */
+ Buf strtab; buf_init(&strtab, h);
+ {
+ u8 z = 0;
+ buf_write(&strtab, &z, 1);
+ }
+
+ /* The .symtab is built into a contiguous arena buffer of fixed-size
+ * 24-byte records. We don't know the count up front; bound by
+ * (nobjsec section symbols) + (obj symbol count). */
+ u32 nobjsym = 0;
+ {
+ ObjSymIter* it = obj_symiter_new(ob);
+ ObjSymEntry e;
+ while (obj_symiter_next(it, &e)) ++nobjsym;
+ obj_symiter_free(it);
+ }
+ u32 max_syms = 1 + (nobjsec - 1) + nobjsym;
+ u8* symtab = (u8*)arena_alloc(c->scratch,
+ (size_t)ELF64_SYM_SIZE * max_syms,
+ _Alignof(u64));
+ u32 nsyms = 0;
+ memset(&symtab[nsyms * ELF64_SYM_SIZE], 0, ELF64_SYM_SIZE);
+ nsyms = 1; /* index 0: STN_UNDEF */
+
+ /* Helper to emit one Elf64_Sym record at index `idx` into symtab. */
+ #define WRITE_SYM(idx, st_name, st_info, st_other, st_shndx, st_value, st_size) \
+ do { \
+ u8* slot = &symtab[(idx) * ELF64_SYM_SIZE]; \
+ slot[0] = (u8)((st_name)); \
+ slot[1] = (u8)((st_name) >> 8); \
+ slot[2] = (u8)((st_name) >> 16); \
+ slot[3] = (u8)((st_name) >> 24); \
+ slot[4] = (u8)((st_info)); \
+ slot[5] = (u8)((st_other)); \
+ slot[6] = (u8)((st_shndx)); \
+ slot[7] = (u8)((st_shndx) >> 8); \
+ for (int _b = 0; _b < 8; ++_b) \
+ slot[8 + _b] = (u8)((u64)(st_value) >> (_b * 8)); \
+ for (int _b = 0; _b < 8; ++_b) \
+ slot[16 + _b] = (u8)((u64)(st_size) >> (_b * 8)); \
+ } while (0)
+
+ /* No automatic STT_SECTION synthesis. Section symbols are emitted
+ * iff they are present in the input ObjBuilder (typically as
+ * SK_SECTION ObjSyms preserved by read_elf, or added explicitly by
+ * a hand-built caller that needs to reference a section by sym).
+ * This matches clang's output: only sections referenced by section
+ * symbols carry one. */
+
+ /* Map obj symbol id -> elf symbol index. */
+ u32* sym_to_elf = arena_array(c->scratch, u32, nobjsym + 2);
+ memset(sym_to_elf, 0, sizeof(u32) * (nobjsym + 2));
+
+ /* Two passes over obj symbols: locals, then globals/weak. */
+ for (int pass = 0; pass < 2; ++pass) {
+ ObjSymIter* it = obj_symiter_new(ob);
+ ObjSymEntry e;
+ while (obj_symiter_next(it, &e)) {
+ const ObjSym* s = e.sym;
+ int is_local = (s->bind == SB_LOCAL);
+ if ((pass == 0) != is_local) continue;
+ u32 nlen;
+ const char* nm = sym_to_str(c, s->name, &nlen);
+ u32 nameoff = nlen ? strtab_add(&strtab, nm, nlen) : 0;
+ u8 info = ELF64_ST_INFO(sym_bind_to_elf(s->bind),
+ sym_kind_to_elf(s->kind));
+ u8 other = sym_vis_to_elf(s->vis);
+ u16 shndx = sym_shndx(s, obj_to_elf, nobjsec);
+ u64 value = (s->kind == SK_COMMON) ? s->common_align : s->value;
+ WRITE_SYM(nsyms, nameoff, info, other, shndx, value, s->size);
+ sym_to_elf[e.id] = nsyms;
+ nsyms++;
+ }
+ obj_symiter_free(it);
+ }
+ #undef WRITE_SYM
+
+ /* sh_info on .symtab is the index of the first non-local symbol.
+ * Locals = 1 (STN_UNDEF) + count of input-side LOCAL obj symbols. */
+ u32 nlocals = 1;
+ {
+ ObjSymIter* it = obj_symiter_new(ob);
+ ObjSymEntry e;
+ while (obj_symiter_next(it, &e)) {
+ if (e.sym->bind == SB_LOCAL) ++nlocals;
+ }
+ obj_symiter_free(it);
+ }
+
+ /* Append .symtab + .strtab + .shstrtab planning records.
+ * sh_link/sh_info for .symtab and .rela.* are filled in once we know
+ * each section's elf index. */
+ u32 idx_symtab = 0, idx_strtab = 0, idx_shstrtab = 0;
+
+ /* ---- pass 3: build .rela.<name> contents ------------------------ */
+
+ /* Allocate one .rela section per obj section that has any relocs. */
+ u32 total_relocs = 0;
+ for (u32 i = 1; i < nobjsec; ++i) total_relocs += obj_reloc_count(ob, i);
+ const Reloc* all_relocs = total_relocs ? obj_relocs(ob, 0) : NULL;
+
+ typedef struct RelaPlan {
+ u32 obj_section; /* obj section the rela applies to */
+ u8* bytes; /* arena-allocated rela bytes */
+ u32 size; /* bytes count = nrelocs * 24 */
+ } RelaPlan;
+
+ RelaPlan* rela_plans = arena_array(c->scratch, RelaPlan, nobjsec);
+ memset(rela_plans, 0, sizeof(RelaPlan) * nobjsec);
+ u32 nrela_plans = 0;
+
+ for (u32 si = 1; si < nobjsec; ++si) {
+ u32 nr = obj_reloc_count(ob, si);
+ if (!nr) continue;
+ u8* buf = (u8*)arena_alloc(c->scratch,
+ (size_t)ELF64_RELA_SIZE * nr,
+ _Alignof(u64));
+ u32 j = 0;
+ for (u32 i = 0; i < total_relocs; ++i) {
+ const Reloc* r = &all_relocs[i];
+ if (r->section_id != si) continue;
+ u32 etype = elf_aarch64_reloc_to(r->kind);
+ if (etype == ELF_R_AARCH64_NONE && r->kind != R_NONE) {
+ compiler_panic(c, no_loc(),
+ "emit_elf: unsupported relocation kind %u for AArch64",
+ (u32)r->kind);
+ }
+ u32 sym_elf_idx;
+ if (r->sym == OBJ_SYM_NONE) {
+ /* Reloc against a section: use the synthesized
+ * STT_SECTION symbol if the obj reloc carries a
+ * section_id-equivalent; otherwise 0. */
+ sym_elf_idx = 0;
+ } else {
+ sym_elf_idx = sym_to_elf[r->sym];
+ }
+ u8* slot = &buf[j * ELF64_RELA_SIZE];
+ for (int b = 0; b < 8; ++b)
+ slot[b] = (u8)((u64)r->offset >> (b * 8));
+ u64 info = ELF64_R_INFO(sym_elf_idx, etype);
+ for (int b = 0; b < 8; ++b)
+ slot[8 + b] = (u8)(info >> (b * 8));
+ for (int b = 0; b < 8; ++b)
+ slot[16 + b] = (u8)((u64)r->addend >> (b * 8));
+ ++j;
+ }
+ rela_plans[nrela_plans].obj_section = si;
+ rela_plans[nrela_plans].bytes = buf;
+ rela_plans[nrela_plans].size = nr * ELF64_RELA_SIZE;
+ nrela_plans++;
+ }
+
+ /* Append ElfSec entries for each .rela.<name>. Names are ".rela" +
+ * the obj section name; allocate in scratch. */
+ u32* rela_elf_idx = arena_array(c->scratch, u32, nrela_plans + 1);
+ for (u32 ri = 0; ri < nrela_plans; ++ri) {
+ u32 si = rela_plans[ri].obj_section;
+ const Section* s = obj_section_get(ob, si);
+ u32 base_len;
+ const char* base = sym_to_str(c, s->name, &base_len);
+ u32 nlen = 5 + base_len; /* ".rela" + base */
+ char* nm = (char*)arena_alloc(c->scratch, nlen + 1, 1);
+ memcpy(nm, ".rela", 5);
+ memcpy(nm + 5, base, base_len);
+ nm[nlen] = 0;
+
+ ElfSec* es = &secs[nsecs];
+ memset(es, 0, sizeof *es);
+ es->name = nm;
+ es->name_len = nlen;
+ es->sh_type = SHT_RELA;
+ es->sh_flags = SHF_INFO_LINK;
+ es->sh_addralign = 8;
+ es->sh_entsize = ELF64_RELA_SIZE;
+ es->sh_info = obj_to_elf[si]; /* section the relas apply to */
+ /* sh_link filled below once we know symtab's elf index. */
+ es->raw_bytes = rela_plans[ri].bytes;
+ es->sh_size = rela_plans[ri].size;
+ rela_elf_idx[ri] = nsecs;
+ nsecs++;
+ }
+
+ /* Append .symtab. */
+ {
+ ElfSec* es = &secs[nsecs];
+ memset(es, 0, sizeof *es);
+ es->name = ".symtab";
+ es->name_len = 7;
+ es->sh_type = SHT_SYMTAB;
+ es->sh_flags = 0;
+ es->sh_addralign = 8;
+ es->sh_entsize = ELF64_SYM_SIZE;
+ es->raw_bytes = symtab;
+ es->sh_size = (u64)nsyms * ELF64_SYM_SIZE;
+ es->sh_info = nlocals; /* first non-local symbol */
+ idx_symtab = nsecs;
+ nsecs++;
+ }
+
+ /* Patch sh_link on each .rela section now that we have idx_symtab. */
+ for (u32 ri = 0; ri < nrela_plans; ++ri) {
+ secs[rela_elf_idx[ri]].sh_link = idx_symtab;
+ }
+
+ /* ---- pass 4: append section names to the same strtab and emit it.
+ *
+ * clang reuses .strtab for both symbol names and section names —
+ * e_shstrndx and .symtab.sh_link both point at it. Match that
+ * convention: continue appending into `strtab` (which already
+ * contains the symbol names), then emit one STRTAB section. */
+
+ /* secs[0] (SHN_UNDEF) carries name "" → offset 0. */
+ secs[0].sh_name = 0;
+ for (u32 i = 1; i < nsecs; ++i) {
+ secs[i].sh_name = strtab_add(&strtab, secs[i].name, secs[i].name_len);
+ }
+
+ /* Append the .strtab section record itself; its own name lands in
+ * the same buffer (so the strtab is self-describing). */
+ {
+ const char* nm = ".strtab";
+ u32 nlen = 7;
+ u32 nameoff = strtab_add(&strtab, nm, nlen);
+ u32 sz = buf_pos(&strtab);
+ u8* flat = (u8*)arena_alloc(c->scratch, sz, 1);
+ buf_flatten(&strtab, flat);
+ buf_fini(&strtab);
+
+ ElfSec* es = &secs[nsecs];
+ memset(es, 0, sizeof *es);
+ es->name = nm;
+ es->name_len = nlen;
+ es->sh_name = nameoff;
+ es->sh_type = SHT_STRTAB;
+ es->sh_addralign = 1;
+ es->raw_bytes = flat;
+ es->sh_size = sz;
+ idx_strtab = nsecs;
+ idx_shstrtab = nsecs; /* same section serves both roles */
+ nsecs++;
+ }
+ secs[idx_symtab].sh_link = idx_strtab;
+
+ /* ---- pass 5: assign file offsets -------------------------------- */
+
+ u64 cur = ELF64_EHDR_SIZE;
+ for (u32 i = 1; i < nsecs; ++i) {
+ ElfSec* es = &secs[i];
+ if (es->is_nobits) {
+ /* sh_offset for NOBITS is conventionally where the next
+ * non-NOBITS section begins; we set it to cur without
+ * advancing. */
+ es->sh_offset = cur;
+ continue;
+ }
+ u64 a = es->sh_addralign ? es->sh_addralign : 1;
+ cur = align_up64(cur, a);
+ es->sh_offset = cur;
+ cur += es->sh_size;
+ }
+ cur = align_up64(cur, 8);
+ u64 e_shoff = cur;
+
+ /* ---- pass 6: write Ehdr ----------------------------------------- */
+
+ u8 ident[EI_NIDENT] = {0};
+ ident[EI_MAG0] = ELFMAG0;
+ ident[EI_MAG1] = ELFMAG1;
+ ident[EI_MAG2] = ELFMAG2;
+ ident[EI_MAG3] = ELFMAG3;
+ ident[EI_CLASS] = ELFCLASS64;
+ ident[EI_DATA] = ELFDATA2LSB;
+ ident[EI_VERSION] = EV_CURRENT;
+ /* SysV is the canonical OSABI for relocatable AArch64 .o; clang and
+ * 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. */
+ ident[EI_OSABI] = ELFOSABI_NONE;
+ cfree_writer_seek(w, 0);
+ cfree_writer_write(w, ident, EI_NIDENT);
+ elf_wr_u16(w, ET_REL);
+ elf_wr_u16(w, EM_AARCH64);
+ elf_wr_u32(w, EV_CURRENT);
+ elf_wr_u64(w, 0); /* e_entry */
+ elf_wr_u64(w, 0); /* e_phoff */
+ elf_wr_u64(w, e_shoff); /* e_shoff */
+ elf_wr_u32(w, 0); /* e_flags */
+ elf_wr_u16(w, ELF64_EHDR_SIZE); /* e_ehsize */
+ elf_wr_u16(w, 0); /* e_phentsize */
+ elf_wr_u16(w, 0); /* e_phnum */
+ elf_wr_u16(w, ELF64_SHDR_SIZE); /* e_shentsize */
+ elf_wr_u16(w, (u16)nsecs); /* e_shnum */
+ elf_wr_u16(w, (u16)idx_shstrtab); /* e_shstrndx */
+
+ /* ---- pass 7: write each section's bytes ------------------------- */
+
+ for (u32 i = 1; i < nsecs; ++i) {
+ ElfSec* es = &secs[i];
+ if (es->is_nobits || es->sh_size == 0) continue;
+ cfree_writer_seek(w, es->sh_offset);
+ if (es->obj_bytes) {
+ u32 sz = es->obj_bytes->total;
+ u8* tmp = (u8*)h->alloc(h, sz ? sz : 1, 1);
+ if (sz) buf_flatten(es->obj_bytes, tmp);
+ cfree_writer_write(w, tmp, sz);
+ h->free(h, tmp, sz ? sz : 1);
+ } else if (es->raw_bytes) {
+ cfree_writer_write(w, es->raw_bytes, (size_t)es->sh_size);
+ }
+ }
+
+ /* ---- pass 8: write section header table ------------------------- */
+
+ cfree_writer_seek(w, e_shoff);
+ for (u32 i = 0; i < nsecs; ++i) {
+ const ElfSec* es = &secs[i];
+ elf_wr_u32(w, es->sh_name);
+ elf_wr_u32(w, es->sh_type);
+ elf_wr_u64(w, es->sh_flags);
+ elf_wr_u64(w, es->sh_addr);
+ elf_wr_u64(w, es->sh_offset);
+ elf_wr_u64(w, es->sh_size);
+ elf_wr_u32(w, es->sh_link);
+ elf_wr_u32(w, es->sh_info);
+ elf_wr_u64(w, es->sh_addralign);
+ elf_wr_u64(w, es->sh_entsize);
+ }
+
+ (void)align_up;
+}
diff --git a/src/obj/elf_read.c b/src/obj/elf_read.c
@@ -0,0 +1,426 @@
+/* ELF ET_REL reader. Parses a 64-bit little-endian relocatable object
+ * back into a fresh ObjBuilder. The post-finalize ObjBuilder shape is
+ * the canonical superset doc/DESIGN.md §5.5 promises: read_elf of an
+ * emit_elf output produces an ObjBuilder equivalent to the writer's
+ * input, modulo (a) section ordering and (b) STT_SECTION symbols
+ * synthesized by the writer.
+ *
+ * Scope: AArch64 little-endian. Other archs / endianness produce a
+ * compiler_panic with a diagnostic. */
+
+#include "obj/elf.h"
+
+#include "core/heap.h"
+#include "core/pool.h"
+
+#include <string.h>
+
+static SrcLoc no_loc(void) { SrcLoc l = {0,0,0}; return l; }
+
+/* ---- shdr scratch struct ---- */
+
+typedef struct ShdrRec {
+ u32 sh_name;
+ u32 sh_type;
+ u64 sh_flags;
+ u64 sh_addr;
+ u64 sh_offset;
+ u64 sh_size;
+ u32 sh_link;
+ u32 sh_info;
+ u64 sh_addralign;
+ u64 sh_entsize;
+} ShdrRec;
+
+static void parse_shdr(const u8* p, ShdrRec* out)
+{
+ out->sh_name = elf_rd_u32(p + 0);
+ out->sh_type = elf_rd_u32(p + 4);
+ out->sh_flags = elf_rd_u64(p + 8);
+ out->sh_addr = elf_rd_u64(p + 16);
+ out->sh_offset = elf_rd_u64(p + 24);
+ out->sh_size = elf_rd_u64(p + 32);
+ out->sh_link = elf_rd_u32(p + 40);
+ out->sh_info = elf_rd_u32(p + 44);
+ out->sh_addralign = elf_rd_u64(p + 48);
+ out->sh_entsize = elf_rd_u64(p + 56);
+}
+
+/* ---- mappers ---- */
+
+static u16 elf_flags_to_obj(u64 f)
+{
+ u16 r = 0;
+ if (f & SHF_ALLOC) r |= SF_ALLOC;
+ if (f & SHF_EXECINSTR) r |= SF_EXEC;
+ if (f & SHF_WRITE) r |= SF_WRITE;
+ if (f & SHF_TLS) r |= SF_TLS;
+ if (f & SHF_MERGE) r |= SF_MERGE;
+ if (f & SHF_STRINGS) r |= SF_STRINGS;
+ if (f & SHF_GROUP) r |= SF_GROUP;
+ if (f & SHF_LINK_ORDER) r |= SF_LINK_ORDER;
+ return r;
+}
+
+static u16 elf_type_to_sem(u32 t)
+{
+ switch (t) {
+ case SHT_PROGBITS: return SSEM_PROGBITS;
+ case SHT_NOBITS: return SSEM_NOBITS;
+ case SHT_SYMTAB: return SSEM_SYMTAB;
+ case SHT_STRTAB: return SSEM_STRTAB;
+ case SHT_RELA: return SSEM_RELA;
+ case SHT_REL: return SSEM_REL;
+ case SHT_NOTE: return SSEM_NOTE;
+ case SHT_INIT_ARRAY: return SSEM_INIT_ARRAY;
+ case SHT_FINI_ARRAY: return SSEM_FINI_ARRAY;
+ case SHT_PREINIT_ARRAY: return SSEM_PREINIT_ARRAY;
+ case SHT_GROUP: return SSEM_GROUP;
+ default: return SSEM_PROGBITS;
+ }
+}
+
+static u16 elf_kind_from_name(const char* name, u32 nlen, u64 sh_flags,
+ u32 sh_type)
+{
+ if (sh_type == SHT_NOBITS) return SEC_BSS;
+ if (nlen >= 5 && memcmp(name, ".text", 5) == 0) return SEC_TEXT;
+ if (nlen >= 7 && memcmp(name, ".rodata", 7) == 0) return SEC_RODATA;
+ if (nlen >= 5 && memcmp(name, ".data", 5) == 0) return SEC_DATA;
+ if (nlen >= 4 && memcmp(name, ".bss", 4) == 0) return SEC_BSS;
+ if (nlen >= 7 && memcmp(name, ".debug_", 7) == 0) return SEC_DEBUG;
+ /* Fallback: classify by flags. */
+ if (sh_flags & SHF_EXECINSTR) return SEC_TEXT;
+ if (sh_flags & SHF_WRITE) return SEC_DATA;
+ if (sh_flags & SHF_ALLOC) return SEC_RODATA;
+ return SEC_OTHER;
+}
+
+static u16 elf_bind_to_obj(u32 b)
+{
+ switch (b) {
+ case STB_GLOBAL: return SB_GLOBAL;
+ case STB_WEAK: return SB_WEAK;
+ default: return SB_LOCAL;
+ }
+}
+
+static u16 elf_type_to_kind(u32 t, u16 shndx)
+{
+ if (shndx == SHN_UNDEF) return SK_UNDEF;
+ if (shndx == SHN_COMMON) return SK_COMMON;
+ /* SHN_ABS is the convention for STT_FILE and a few other defined
+ * symbols whose value is not an address. Don't smother the type
+ * with SK_ABS when the type field carries real information — only
+ * fall through to SK_ABS for STT_NOTYPE-at-SHN_ABS. */
+ if (shndx == SHN_ABS && t == STT_NOTYPE) return SK_ABS;
+ switch (t) {
+ case STT_FUNC: return SK_FUNC;
+ case STT_OBJECT: return SK_OBJ;
+ case STT_SECTION: return SK_SECTION;
+ case STT_FILE: return SK_FILE;
+ case STT_TLS: return SK_TLS;
+ case STT_COMMON: return SK_COMMON;
+ default:
+ /* STT_NOTYPE on a defined symbol (e.g. AArch64 mapping symbols
+ * `$x` / `$d`, or assembly labels) round-trips as SK_NOTYPE.
+ * The linker keeps definedness keyed on SK_UNDEF; SK_NOTYPE is
+ * "defined but typeless". */
+ return SK_NOTYPE;
+ }
+}
+
+static u8 elf_other_to_vis(u32 other)
+{
+ switch (other & 3) {
+ case STV_HIDDEN: return SV_HIDDEN;
+ case STV_PROTECTED: return SV_PROTECTED;
+ case STV_INTERNAL: return SV_INTERNAL;
+ default: return SV_DEFAULT;
+ }
+}
+
+/* Bounds-checked C-string slice from a strtab section. Returns "" on
+ * out-of-range so callers don't have to special-case it. `len_out` is
+ * set to strlen(result). */
+static const char* strtab_lookup(const u8* tab, u64 tab_size, u32 off,
+ u32* len_out)
+{
+ if (off >= tab_size) { *len_out = 0; return ""; }
+ const char* s = (const char*)(tab + off);
+ u32 max = (u32)(tab_size - off);
+ u32 n = 0;
+ while (n < max && s[n] != '\0') ++n;
+ *len_out = n;
+ return s;
+}
+
+ObjBuilder* read_elf(Compiler* c, const char* name,
+ const u8* data, size_t len)
+{
+ (void)name;
+
+ if (len < ELF64_EHDR_SIZE)
+ compiler_panic(c, no_loc(), "read_elf: input shorter than ELF header");
+
+ if (data[EI_MAG0] != ELFMAG0 || data[EI_MAG1] != ELFMAG1 ||
+ data[EI_MAG2] != ELFMAG2 || data[EI_MAG3] != ELFMAG3)
+ compiler_panic(c, no_loc(), "read_elf: bad ELF magic");
+
+ if (data[EI_CLASS] != ELFCLASS64)
+ compiler_panic(c, no_loc(),
+ "read_elf: not ELFCLASS64 (got %u)", data[EI_CLASS]);
+ if (data[EI_DATA] != ELFDATA2LSB)
+ compiler_panic(c, no_loc(),
+ "read_elf: not ELFDATA2LSB (got %u)", data[EI_DATA]);
+
+ u16 e_machine = elf_rd_u16(data + 18);
+ if (e_machine != EM_AARCH64)
+ compiler_panic(c, no_loc(),
+ "read_elf: unsupported e_machine 0x%x (only AArch64)",
+ (u32)e_machine);
+
+ u64 e_shoff = elf_rd_u64(data + 40);
+ u16 e_shentsize = elf_rd_u16(data + 58);
+ u16 e_shnum = elf_rd_u16(data + 60);
+ u16 e_shstrndx = elf_rd_u16(data + 62);
+
+ if (e_shentsize != ELF64_SHDR_SIZE)
+ compiler_panic(c, no_loc(),
+ "read_elf: unexpected e_shentsize %u", (u32)e_shentsize);
+ if (e_shoff + (u64)e_shnum * ELF64_SHDR_SIZE > len)
+ compiler_panic(c, no_loc(),
+ "read_elf: section header table out of range");
+ if (e_shstrndx >= e_shnum)
+ compiler_panic(c, no_loc(),
+ "read_elf: e_shstrndx %u >= e_shnum %u",
+ (u32)e_shstrndx, (u32)e_shnum);
+
+ /* Parse all shdrs into scratch. */
+ ShdrRec* shdrs = arena_array(c->scratch, ShdrRec, e_shnum);
+ for (u32 i = 0; i < e_shnum; ++i)
+ parse_shdr(data + e_shoff + (u64)i * ELF64_SHDR_SIZE, &shdrs[i]);
+
+ const ShdrRec* shstr_sh = &shdrs[e_shstrndx];
+ if (shstr_sh->sh_offset + shstr_sh->sh_size > len)
+ compiler_panic(c, no_loc(),
+ "read_elf: .shstrtab out of range");
+ const u8* shstrtab = data + shstr_sh->sh_offset;
+ u64 shstrtab_sz = shstr_sh->sh_size;
+
+ /* Build the ObjBuilder. */
+ ObjBuilder* ob = obj_new(c);
+ if (!ob) compiler_panic(c, no_loc(), "read_elf: obj_new failed");
+
+ /* elf_to_obj[shndx] -> ObjSecId, OBJ_SEC_NONE for skipped sections. */
+ u32* elf_to_obj = arena_array(c->scratch, u32, e_shnum);
+ memset(elf_to_obj, 0, sizeof(u32) * e_shnum);
+
+ /* Pass 1: create obj sections for every non-NULL shdr that carries
+ * load-bearing model state. SYMTAB / STRTAB / RELA / REL are
+ * consumed below for symbols and relocations and do NOT round-trip
+ * as obj sections — emit_elf re-synthesizes them from the
+ * ObjBuilder's symbols / strtab / relocs. The shstrtab is a STRTAB
+ * too, so it falls out the same way. */
+ for (u32 i = 1; i < e_shnum; ++i) {
+ const ShdrRec* sh = &shdrs[i];
+ if (sh->sh_type == SHT_NULL) continue;
+ if (sh->sh_type == SHT_SYMTAB) continue;
+ if (sh->sh_type == SHT_STRTAB) continue;
+ if (sh->sh_type == SHT_RELA) continue;
+ if (sh->sh_type == SHT_REL) continue;
+
+ u32 nlen;
+ const char* nm = strtab_lookup(shstrtab, shstrtab_sz, sh->sh_name, &nlen);
+ Sym sym = pool_intern(c->global, nm, nlen);
+
+ u16 sec_kind = elf_kind_from_name(nm, nlen, sh->sh_flags, sh->sh_type);
+ u16 sec_sem = elf_type_to_sem(sh->sh_type);
+ u16 flags = elf_flags_to_obj(sh->sh_flags);
+ u32 align = sh->sh_addralign ? (u32)sh->sh_addralign : 1;
+
+ ObjSecId id = obj_section_ex(ob, sym, (SecKind)sec_kind, (SecSem)sec_sem,
+ flags, align, (u32)sh->sh_entsize,
+ sh->sh_link, sh->sh_info);
+ if (id == OBJ_SEC_NONE)
+ compiler_panic(c, no_loc(),
+ "read_elf: obj_section_ex failed for '%s'", nm);
+ elf_to_obj[i] = id;
+
+ /* Body bytes. */
+ if (sh->sh_type == SHT_NOBITS) {
+ obj_reserve_bss(ob, id, (u32)sh->sh_size, align);
+ } else if (sh->sh_size) {
+ if (sh->sh_offset + sh->sh_size > len)
+ compiler_panic(c, no_loc(),
+ "read_elf: section '%s' bytes out of range", nm);
+ /* For SYMTAB/STRTAB/RELA we still copy the raw bytes — the
+ * post-finalize shape contract says these sections are
+ * present; emit_elf will regenerate them on re-emit, so the
+ * preserved bytes are informational rather than load-bearing.
+ */
+ obj_write(ob, id, data + sh->sh_offset, (size_t)sh->sh_size);
+ }
+ }
+
+ /* Pass 2: parse the .symtab into ObjSyms, building an
+ * elf_sym_idx -> ObjSymId table. There may be zero or one SYMTAB in
+ * an ET_REL; pick the first. */
+ u32 symtab_shndx = 0;
+ for (u32 i = 1; i < e_shnum; ++i) {
+ if (shdrs[i].sh_type == SHT_SYMTAB) { symtab_shndx = i; break; }
+ }
+
+ u32 nsyms = 0;
+ u32* sym_elf_to_obj = NULL;
+
+ if (symtab_shndx) {
+ const ShdrRec* sh = &shdrs[symtab_shndx];
+ if (sh->sh_entsize != ELF64_SYM_SIZE)
+ compiler_panic(c, no_loc(),
+ "read_elf: .symtab entsize %llu != %u",
+ (unsigned long long)sh->sh_entsize,
+ (u32)ELF64_SYM_SIZE);
+ if (sh->sh_size % ELF64_SYM_SIZE)
+ compiler_panic(c, no_loc(),
+ "read_elf: .symtab size %llu not a multiple of %u",
+ (unsigned long long)sh->sh_size,
+ (u32)ELF64_SYM_SIZE);
+ if (sh->sh_link >= e_shnum)
+ compiler_panic(c, no_loc(),
+ "read_elf: .symtab sh_link %u out of range",
+ sh->sh_link);
+ const ShdrRec* str_sh = &shdrs[sh->sh_link];
+ if (str_sh->sh_offset + str_sh->sh_size > len)
+ compiler_panic(c, no_loc(), "read_elf: .strtab out of range");
+ const u8* strtab = data + str_sh->sh_offset;
+ u64 strtab_sz = str_sh->sh_size;
+
+ nsyms = (u32)(sh->sh_size / ELF64_SYM_SIZE);
+ sym_elf_to_obj = arena_array(c->scratch, u32, nsyms ? nsyms : 1);
+ memset(sym_elf_to_obj, 0, sizeof(u32) * (nsyms ? nsyms : 1));
+
+ const u8* base = data + sh->sh_offset;
+ for (u32 i = 1; i < nsyms; ++i) { /* skip index 0 */
+ const u8* p = base + (u64)i * ELF64_SYM_SIZE;
+ u32 st_name = elf_rd_u32(p + 0);
+ u8 st_info = p[4];
+ u8 st_other = p[5];
+ u16 st_shndx = elf_rd_u16(p + 6);
+ u64 st_value = elf_rd_u64(p + 8);
+ u64 st_size = elf_rd_u64(p + 16);
+
+ u32 nlen;
+ const char* nm = strtab_lookup(strtab, strtab_sz, st_name, &nlen);
+ Sym sn = nlen ? pool_intern(c->global, nm, nlen) : 0;
+
+ u32 e_bind = ELF64_ST_BIND(st_info);
+ u32 e_type = ELF64_ST_TYPE(st_info);
+ u16 bind = elf_bind_to_obj(e_bind);
+ u16 kind = elf_type_to_kind(e_type, st_shndx);
+ u8 vis = elf_other_to_vis(st_other);
+
+ ObjSecId sec_id;
+ u64 value;
+ u64 cmnalign = 0;
+ if (st_shndx == SHN_UNDEF) {
+ sec_id = OBJ_SEC_NONE;
+ value = st_value;
+ } else if (st_shndx == SHN_ABS || st_shndx == SHN_COMMON) {
+ sec_id = OBJ_SEC_NONE;
+ value = st_value;
+ if (st_shndx == SHN_COMMON) cmnalign = st_value;
+ } else if (st_shndx < e_shnum) {
+ sec_id = elf_to_obj[st_shndx];
+ value = st_value;
+ } else {
+ compiler_panic(c, no_loc(),
+ "read_elf: symbol shndx %u out of range",
+ (u32)st_shndx);
+ sec_id = OBJ_SEC_NONE; value = 0; /* unreachable */
+ }
+
+ ObjSymId id = obj_symbol_ex(ob, sn, (SymBind)bind, (SymVis)vis,
+ (SymKind)kind, sec_id, value, st_size,
+ cmnalign);
+ sym_elf_to_obj[i] = id;
+ }
+ }
+
+ /* Pass 3: parse each SHT_RELA / SHT_REL into ObjBuilder relocations
+ * targeting the section the rela header's sh_info points at. */
+ for (u32 i = 1; i < e_shnum; ++i) {
+ const ShdrRec* sh = &shdrs[i];
+ int is_rela = (sh->sh_type == SHT_RELA);
+ int is_rel = (sh->sh_type == SHT_REL);
+ if (!is_rela && !is_rel) continue;
+
+ u32 entsize = is_rela ? ELF64_RELA_SIZE : 16;
+ if (sh->sh_entsize != entsize)
+ compiler_panic(c, no_loc(),
+ "read_elf: rela entsize %llu != %u",
+ (unsigned long long)sh->sh_entsize, entsize);
+ if (sh->sh_info == 0 || sh->sh_info >= e_shnum)
+ compiler_panic(c, no_loc(),
+ "read_elf: rela sh_info %u out of range",
+ sh->sh_info);
+ ObjSecId target = elf_to_obj[sh->sh_info];
+ if (target == OBJ_SEC_NONE) continue;
+
+ u32 nrec = (u32)(sh->sh_size / entsize);
+ const u8* base = data + sh->sh_offset;
+ for (u32 j = 0; j < nrec; ++j) {
+ const u8* p = base + (u64)j * entsize;
+ u64 r_offset = elf_rd_u64(p + 0);
+ u64 r_info = elf_rd_u64(p + 8);
+ i64 r_addend = is_rela ? (i64)elf_rd_u64(p + 16) : 0;
+ u32 esym = ELF64_R_SYM(r_info);
+ u32 etype = ELF64_R_TYPE(r_info);
+
+ u32 kind = elf_aarch64_reloc_from(etype);
+ if (kind == (u32)-1)
+ compiler_panic(c, no_loc(),
+ "read_elf: unsupported AArch64 reloc type %u",
+ etype);
+
+ ObjSymId target_sym = OBJ_SYM_NONE;
+ if (esym && sym_elf_to_obj && esym < nsyms)
+ target_sym = sym_elf_to_obj[esym];
+
+ obj_reloc_ex(ob, target, (u32)r_offset, (RelocKind)kind,
+ target_sym, r_addend, is_rela ? 1 : 0, 0);
+ }
+ }
+
+ /* Pass 4: SHT_GROUP. Each GROUP section's body is a sequence of
+ * 4-byte LE indices: [flags, shndx, shndx, ...]. The signature is
+ * the symbol named by sh_link/sh_info convention (sh_link=symtab,
+ * sh_info=symbol index in that symtab). */
+ for (u32 i = 1; i < e_shnum; ++i) {
+ const ShdrRec* sh = &shdrs[i];
+ if (sh->sh_type != SHT_GROUP) continue;
+
+ if (sh->sh_size < 4 || (sh->sh_size % 4)) continue;
+ const u8* p = data + sh->sh_offset;
+ u32 flags = elf_rd_u32(p);
+ u32 nm_len;
+ const char* gnm = strtab_lookup(shstrtab, shstrtab_sz,
+ sh->sh_name, &nm_len);
+ Sym gname = pool_intern(c->global, gnm, nm_len);
+
+ ObjSymId signature = OBJ_SYM_NONE;
+ if (sym_elf_to_obj && sh->sh_info < nsyms)
+ signature = sym_elf_to_obj[sh->sh_info];
+
+ ObjGroupId gid = obj_group(ob, gname, signature, flags);
+ u32 n = (u32)(sh->sh_size / 4) - 1;
+ for (u32 j = 0; j < n; ++j) {
+ u32 shndx = elf_rd_u32(p + 4 + j * 4);
+ if (shndx < e_shnum && elf_to_obj[shndx] != OBJ_SEC_NONE)
+ obj_group_add_section(ob, gid, elf_to_obj[shndx]);
+ }
+ }
+
+ obj_finalize(ob);
+ return ob;
+}
diff --git a/src/obj/elf_reloc_aarch64.c b/src/obj/elf_reloc_aarch64.c
@@ -0,0 +1,54 @@
+/* RelocKind <-> AArch64 ELF reloc-type mapping.
+ *
+ * Cfree's RelocKind enum is arch-agnostic at its top (R_ABS, R_REL, R_PC
+ * variants) and arch-specific in its lower entries. On AArch64, R_REL and
+ * R_PC collapse to ELF_R_AARCH64_PREL32 / ELF_R_AARCH64_PREL64 — both
+ * mean "PC-relative relative to the symbol" once the linker has resolved
+ * final addresses.
+ *
+ * Returning 0 (ELF_R_AARCH64_NONE) for an unsupported kind is the signal
+ * to the caller to either panic (emit) or panic (read with diagnostic). */
+
+#include "obj/elf.h"
+
+u32 elf_aarch64_reloc_to(u32 kind /* RelocKind */)
+{
+ switch (kind) {
+ case R_NONE: return ELF_R_AARCH64_NONE;
+ case R_ABS64: return ELF_R_AARCH64_ABS64;
+ case R_ABS32: return ELF_R_AARCH64_ABS32;
+ case R_PC64: return ELF_R_AARCH64_PREL64;
+ case R_PC32: return ELF_R_AARCH64_PREL32;
+ case R_REL64: return ELF_R_AARCH64_PREL64;
+ case R_REL32: return ELF_R_AARCH64_PREL32;
+ case R_AARCH64_CALL26: return ELF_R_AARCH64_CALL26;
+ case R_AARCH64_ADR_PREL_PG_HI21: return ELF_R_AARCH64_ADR_PREL_PG_HI21;
+ case R_AARCH64_ADD_ABS_LO12_NC: return ELF_R_AARCH64_ADD_ABS_LO12_NC;
+ case R_AARCH64_LDST8_ABS_LO12_NC: return ELF_R_AARCH64_LDST8_ABS_LO12_NC;
+ case R_AARCH64_LDST16_ABS_LO12_NC: return ELF_R_AARCH64_LDST16_ABS_LO12_NC;
+ case R_AARCH64_LDST32_ABS_LO12_NC: return ELF_R_AARCH64_LDST32_ABS_LO12_NC;
+ case R_AARCH64_LDST64_ABS_LO12_NC: return ELF_R_AARCH64_LDST64_ABS_LO12_NC;
+ case R_AARCH64_LDST128_ABS_LO12_NC: return ELF_R_AARCH64_LDST128_ABS_LO12_NC;
+ default: return ELF_R_AARCH64_NONE;
+ }
+}
+
+u32 elf_aarch64_reloc_from(u32 elf_type)
+{
+ switch (elf_type) {
+ case ELF_R_AARCH64_NONE: return R_NONE;
+ case ELF_R_AARCH64_ABS64: return R_ABS64;
+ case ELF_R_AARCH64_ABS32: return R_ABS32;
+ case ELF_R_AARCH64_PREL64: return R_PC64;
+ case ELF_R_AARCH64_PREL32: return R_PC32;
+ case ELF_R_AARCH64_CALL26: return R_AARCH64_CALL26;
+ case ELF_R_AARCH64_ADR_PREL_PG_HI21: return R_AARCH64_ADR_PREL_PG_HI21;
+ case ELF_R_AARCH64_ADD_ABS_LO12_NC: return R_AARCH64_ADD_ABS_LO12_NC;
+ case ELF_R_AARCH64_LDST8_ABS_LO12_NC: return R_AARCH64_LDST8_ABS_LO12_NC;
+ case ELF_R_AARCH64_LDST16_ABS_LO12_NC: return R_AARCH64_LDST16_ABS_LO12_NC;
+ case ELF_R_AARCH64_LDST32_ABS_LO12_NC: return R_AARCH64_LDST32_ABS_LO12_NC;
+ case ELF_R_AARCH64_LDST64_ABS_LO12_NC: return R_AARCH64_LDST64_ABS_LO12_NC;
+ case ELF_R_AARCH64_LDST128_ABS_LO12_NC: return R_AARCH64_LDST128_ABS_LO12_NC;
+ default: return (u32)-1; /* sentinel */
+ }
+}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -0,0 +1,419 @@
+/* In-memory ObjBuilder. Section, symbol, group, and reloc storage all
+ * live in the host heap (env->heap); section bytes use the chunked Buf
+ * type. Index 0 of each id space is reserved as "none".
+ *
+ * obj_finalize is the read-side gate: post-finalize, write-side calls
+ * are still legal (the reader paths use them too) but consumers can
+ * count on the index spaces being stable. */
+
+#include "obj/obj.h"
+
+#include "core/heap.h"
+#include "core/pool.h"
+
+#include <string.h>
+
+struct CfreeObjBuilder {
+ Compiler* c;
+ Heap* heap;
+
+ Section* sections;
+ u32 nsections; /* logical count incl. id-0 sentinel */
+ u32 sections_cap;
+
+ ObjSym* symbols;
+ u32 nsymbols; /* logical count incl. id-0 sentinel */
+ u32 symbols_cap;
+
+ /* Relocs are stored in one flat array; each Section's range can be
+ * recovered by linear walk. Sections are not many (low hundreds at
+ * most) and reloc lookups happen rarely outside emit, so this is
+ * fine and keeps memory layout simple. */
+ Reloc* relocs;
+ u32 nrelocs;
+ u32 relocs_cap;
+
+ ObjGroup* groups;
+ u32 ngroups; /* logical count incl. id-0 sentinel */
+ u32 groups_cap;
+};
+
+struct ObjSymIter {
+ const ObjBuilder* ob;
+ u32 idx; /* next index to return */
+};
+
+/* ---- growth helpers ---- */
+
+#define GROW_AT_LEAST(cap, want) ((cap) ? (cap) : 8)
+
+static int sections_grow(ObjBuilder* ob, u32 want)
+{
+ u32 new_cap;
+ Section* p;
+ if (want <= ob->sections_cap) return 0;
+ new_cap = GROW_AT_LEAST(ob->sections_cap, want);
+ while (new_cap < want) new_cap *= 2;
+ p = (Section*)ob->heap->realloc(
+ ob->heap, ob->sections,
+ sizeof(*ob->sections) * ob->sections_cap,
+ sizeof(*ob->sections) * new_cap,
+ _Alignof(Section));
+ if (!p) return 1;
+ ob->sections = p;
+ ob->sections_cap = new_cap;
+ return 0;
+}
+
+static int symbols_grow(ObjBuilder* ob, u32 want)
+{
+ u32 new_cap;
+ ObjSym* p;
+ if (want <= ob->symbols_cap) return 0;
+ new_cap = GROW_AT_LEAST(ob->symbols_cap, want);
+ while (new_cap < want) new_cap *= 2;
+ p = (ObjSym*)ob->heap->realloc(
+ ob->heap, ob->symbols,
+ sizeof(*ob->symbols) * ob->symbols_cap,
+ sizeof(*ob->symbols) * new_cap,
+ _Alignof(ObjSym));
+ if (!p) return 1;
+ ob->symbols = p;
+ ob->symbols_cap = new_cap;
+ return 0;
+}
+
+static int relocs_grow(ObjBuilder* ob, u32 want)
+{
+ u32 new_cap;
+ Reloc* p;
+ if (want <= ob->relocs_cap) return 0;
+ new_cap = GROW_AT_LEAST(ob->relocs_cap, want);
+ while (new_cap < want) new_cap *= 2;
+ p = (Reloc*)ob->heap->realloc(
+ ob->heap, ob->relocs,
+ sizeof(*ob->relocs) * ob->relocs_cap,
+ sizeof(*ob->relocs) * new_cap,
+ _Alignof(Reloc));
+ if (!p) return 1;
+ ob->relocs = p;
+ ob->relocs_cap = new_cap;
+ return 0;
+}
+
+static int groups_grow(ObjBuilder* ob, u32 want)
+{
+ u32 new_cap;
+ ObjGroup* p;
+ if (want <= ob->groups_cap) return 0;
+ new_cap = GROW_AT_LEAST(ob->groups_cap, want);
+ while (new_cap < want) new_cap *= 2;
+ p = (ObjGroup*)ob->heap->realloc(
+ ob->heap, ob->groups,
+ sizeof(*ob->groups) * ob->groups_cap,
+ sizeof(*ob->groups) * new_cap,
+ _Alignof(ObjGroup));
+ if (!p) return 1;
+ ob->groups = p;
+ ob->groups_cap = new_cap;
+ return 0;
+}
+
+/* ---- lifecycle ---- */
+
+ObjBuilder* obj_new(Compiler* c)
+{
+ Heap* h = (Heap*)c->env->heap;
+ ObjBuilder* ob = (ObjBuilder*)h->alloc(h, sizeof(*ob), _Alignof(ObjBuilder));
+ if (!ob) return NULL;
+ memset(ob, 0, sizeof(*ob));
+ ob->c = c;
+ ob->heap = h;
+
+ /* Reserve index 0 in each id space as the "none" sentinel. */
+ if (sections_grow(ob, 1) || symbols_grow(ob, 1) || groups_grow(ob, 1)) {
+ obj_free(ob);
+ return NULL;
+ }
+ memset(&ob->sections[0], 0, sizeof(ob->sections[0]));
+ memset(&ob->symbols[0], 0, sizeof(ob->symbols[0]));
+ memset(&ob->groups[0], 0, sizeof(ob->groups[0]));
+ ob->nsections = 1;
+ ob->nsymbols = 1;
+ ob->ngroups = 1;
+ return ob;
+}
+
+void obj_free(ObjBuilder* ob)
+{
+ u32 i;
+ if (!ob) return;
+ for (i = 1; i < ob->nsections; ++i) {
+ buf_fini(&ob->sections[i].bytes);
+ }
+ for (i = 1; i < ob->ngroups; ++i) {
+ if (ob->groups[i].sections) {
+ ob->heap->free(ob->heap, ob->groups[i].sections,
+ sizeof(ObjSecId) * ob->groups[i].nsections);
+ }
+ }
+ if (ob->sections) ob->heap->free(ob->heap, ob->sections,
+ sizeof(*ob->sections) * ob->sections_cap);
+ if (ob->symbols) ob->heap->free(ob->heap, ob->symbols,
+ sizeof(*ob->symbols) * ob->symbols_cap);
+ if (ob->relocs) ob->heap->free(ob->heap, ob->relocs,
+ sizeof(*ob->relocs) * ob->relocs_cap);
+ if (ob->groups) ob->heap->free(ob->heap, ob->groups,
+ sizeof(*ob->groups) * ob->groups_cap);
+ ob->heap->free(ob->heap, ob, sizeof(*ob));
+}
+
+/* ---- write side ---- */
+
+ObjSecId obj_section(ObjBuilder* ob, Sym name, SecKind kind, u16 flags, u32 align)
+{
+ return obj_section_ex(ob, name, kind, SSEM_PROGBITS, flags, align,
+ 0, OBJ_SEC_NONE, 0);
+}
+
+ObjSecId obj_section_ex(ObjBuilder* ob, Sym name, SecKind kind, SecSem sem,
+ u16 flags, u32 align, u32 entsize, u32 link, u32 info)
+{
+ ObjSecId id;
+ if (sections_grow(ob, ob->nsections + 1)) return OBJ_SEC_NONE;
+ id = ob->nsections++;
+ {
+ Section* s = &ob->sections[id];
+ memset(s, 0, sizeof(*s));
+ s->name = name;
+ s->kind = (u16)kind;
+ s->flags = flags;
+ s->sem = (u16)sem;
+ s->ext_kind = OBJ_EXT_NONE;
+ s->align = align ? align : 1;
+ s->entsize = entsize;
+ s->link = (ObjSecId)link;
+ s->info = info;
+ s->group_id = OBJ_GROUP_NONE;
+ s->bss_size = 0;
+ buf_init(&s->bytes, ob->heap);
+ }
+ return id;
+}
+
+void obj_section_set_flags(ObjBuilder* ob, ObjSecId id, u16 flags)
+{ if (id != OBJ_SEC_NONE && id < ob->nsections) ob->sections[id].flags = flags; }
+
+void obj_section_set_align(ObjBuilder* ob, ObjSecId id, u32 align)
+{ if (id != OBJ_SEC_NONE && id < ob->nsections) ob->sections[id].align = align ? align : 1; }
+
+void obj_section_set_group(ObjBuilder* ob, ObjSecId id, ObjGroupId gid)
+{ if (id != OBJ_SEC_NONE && id < ob->nsections) ob->sections[id].group_id = gid; }
+
+void obj_write(ObjBuilder* ob, ObjSecId id, const void* data, size_t n)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return;
+ buf_write(&ob->sections[id].bytes, data, n);
+}
+
+u8* obj_reserve(ObjBuilder* ob, ObjSecId id, size_t n)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return NULL;
+ return buf_reserve(&ob->sections[id].bytes, n);
+}
+
+void obj_reserve_bss(ObjBuilder* ob, ObjSecId id, u32 size, u32 align)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return;
+ ob->sections[id].bss_size = size;
+ if (align) ob->sections[id].align = align;
+}
+
+u32 obj_pos(ObjBuilder* ob, ObjSecId id)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return 0;
+ return buf_pos(&ob->sections[id].bytes);
+}
+
+void obj_patch(ObjBuilder* ob, ObjSecId id, u32 ofs, const void* data, size_t n)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return;
+ buf_patch(&ob->sections[id].bytes, ofs, data, n);
+}
+
+ObjSymId obj_symbol(ObjBuilder* ob, Sym name, SymBind bind, SymKind kind,
+ ObjSecId section_id, u64 value, u64 size)
+{
+ return obj_symbol_ex(ob, name, bind, SV_DEFAULT, kind,
+ section_id, value, size, 0);
+}
+
+ObjSymId obj_symbol_ex(ObjBuilder* ob, Sym name, SymBind bind, SymVis vis,
+ SymKind kind, ObjSecId section_id, u64 value, u64 size,
+ u64 common_align)
+{
+ ObjSymId id;
+ if (symbols_grow(ob, ob->nsymbols + 1)) return OBJ_SYM_NONE;
+ id = ob->nsymbols++;
+ {
+ ObjSym* s = &ob->symbols[id];
+ memset(s, 0, sizeof(*s));
+ s->name = name;
+ s->bind = (u16)bind;
+ s->kind = (u16)kind;
+ s->vis = (u8)vis;
+ s->ext_kind = OBJ_EXT_NONE;
+ s->section_id = section_id;
+ s->value = value;
+ s->size = size;
+ s->common_align = common_align;
+ }
+ return id;
+}
+
+void obj_symbol_define(ObjBuilder* ob, ObjSymId id, ObjSecId section_id,
+ u64 value, u64 size)
+{
+ if (id == OBJ_SYM_NONE || id >= ob->nsymbols) return;
+ ob->symbols[id].section_id = section_id;
+ ob->symbols[id].value = value;
+ ob->symbols[id].size = size;
+ if (ob->symbols[id].kind == SK_UNDEF) {
+ ob->symbols[id].kind = SK_OBJ; /* defaults; caller can re-set */
+ }
+}
+
+void obj_reloc(ObjBuilder* ob, ObjSecId section_id, u32 offset,
+ RelocKind kind, ObjSymId sym, i64 addend)
+{
+ obj_reloc_ex(ob, section_id, offset, kind, sym, addend, 1, 0);
+}
+
+void obj_reloc_ex(ObjBuilder* ob, ObjSecId section_id, u32 offset,
+ RelocKind kind, ObjSymId sym, i64 addend,
+ int explicit_addend, int pair)
+{
+ if (relocs_grow(ob, ob->nrelocs + 1)) return;
+ {
+ Reloc* r = &ob->relocs[ob->nrelocs++];
+ r->section_id = section_id;
+ r->offset = offset;
+ r->kind = (u16)kind;
+ r->has_explicit_addend = (u8)(explicit_addend ? 1 : 0);
+ r->pair = (u8)pair;
+ r->sym = sym;
+ r->addend = addend;
+ }
+}
+
+ObjGroupId obj_group(ObjBuilder* ob, Sym name, ObjSymId signature, u32 flags)
+{
+ ObjGroupId id;
+ if (groups_grow(ob, ob->ngroups + 1)) return OBJ_GROUP_NONE;
+ id = ob->ngroups++;
+ {
+ ObjGroup* g = &ob->groups[id];
+ memset(g, 0, sizeof(*g));
+ g->name = name;
+ g->signature = signature;
+ g->flags = flags;
+ }
+ return id;
+}
+
+void obj_group_add_section(ObjBuilder* ob, ObjGroupId gid, ObjSecId sec)
+{
+ ObjGroup* g;
+ ObjSecId* p;
+ if (gid == OBJ_GROUP_NONE || gid >= ob->ngroups) return;
+ g = &ob->groups[gid];
+ /* Linear realloc — group section counts are tiny (handful per group). */
+ p = (ObjSecId*)ob->heap->realloc(
+ ob->heap, g->sections,
+ sizeof(ObjSecId) * g->nsections,
+ sizeof(ObjSecId) * (g->nsections + 1),
+ _Alignof(ObjSecId));
+ if (!p) return;
+ p[g->nsections++] = sec;
+ g->sections = p;
+}
+
+void obj_finalize(ObjBuilder* ob)
+{
+ /* No flat-offset patching needed yet — section bytes are read out via
+ * buf_flatten on demand by emitters. Keep this hook in place: when a
+ * future writer wants intra-section fixups (e.g. label-to-offset
+ * resolution after the full section is written), this is where they
+ * land. */
+ (void)ob;
+}
+
+/* ---- read side ---- */
+
+u32 obj_section_count(const ObjBuilder* ob) { return ob->nsections; }
+
+const Section* obj_section_get(const ObjBuilder* ob, ObjSecId id)
+{
+ if (id == OBJ_SEC_NONE || id >= ob->nsections) return NULL;
+ return &ob->sections[id];
+}
+
+u32 obj_reloc_count(const ObjBuilder* ob, ObjSecId id)
+{
+ u32 i, n = 0;
+ for (i = 0; i < ob->nrelocs; ++i) if (ob->relocs[i].section_id == id) ++n;
+ return n;
+}
+
+const Reloc* obj_relocs(const ObjBuilder* ob, ObjSecId id)
+{
+ /* Returns the first reloc record whose section_id matches; the caller
+ * is expected to walk forward by obj_reloc_count and check
+ * `r->section_id == id` to terminate, since relocs from different
+ * sections may be interleaved.
+ *
+ * That contract is awkward enough that ELF emitters in practice walk
+ * the entire flat array filtering by section. We emulate that by
+ * returning the start of the flat array; callers must filter. */
+ (void)id;
+ return ob->relocs;
+}
+
+const ObjSym* obj_symbol_get(const ObjBuilder* ob, ObjSymId id)
+{
+ if (id == OBJ_SYM_NONE || id >= ob->nsymbols) return NULL;
+ return &ob->symbols[id];
+}
+
+u32 obj_group_count(const ObjBuilder* ob) { return ob->ngroups; }
+
+const ObjGroup* obj_group_get(const ObjBuilder* ob, ObjGroupId id)
+{
+ if (id == OBJ_GROUP_NONE || id >= ob->ngroups) return NULL;
+ return &ob->groups[id];
+}
+
+ObjSymIter* obj_symiter_new(const ObjBuilder* ob)
+{
+ ObjSymIter* it = (ObjSymIter*)ob->heap->alloc(
+ ob->heap, sizeof(*it), _Alignof(ObjSymIter));
+ if (!it) return NULL;
+ it->ob = ob;
+ it->idx = 1; /* skip the id-0 sentinel */
+ return it;
+}
+
+int obj_symiter_next(ObjSymIter* it, ObjSymEntry* out)
+{
+ if (!it || it->idx >= it->ob->nsymbols) return 0;
+ out->id = it->idx;
+ out->sym = &it->ob->symbols[it->idx];
+ it->idx++;
+ return 1;
+}
+
+void obj_symiter_free(ObjSymIter* it)
+{
+ if (!it) return;
+ ((Heap*)it->ob->heap)->free((Heap*)it->ob->heap, it, sizeof(*it));
+}
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -61,6 +61,10 @@ typedef enum SymKind {
SK_COMMON,
SK_TLS,
SK_ABS,
+ /* Defined symbol with no specific type — assembly labels, AArch64
+ * mapping symbols (`$x`, `$d`). Distinct from SK_UNDEF (undefined
+ * external) so the linker keeps definedness keyed on SK_UNDEF. */
+ SK_NOTYPE,
} SymKind;
typedef enum ObjExtKind {
@@ -92,6 +96,9 @@ typedef enum RelocKind {
R_GOT32, R_PLT32,
R_ARM_CALL, R_ARM_MOVW, R_ARM_MOVT, R_ARM_B26,
R_AARCH64_CALL26, R_AARCH64_ADR_PREL_PG_HI21, R_AARCH64_ADD_ABS_LO12_NC,
+ R_AARCH64_LDST8_ABS_LO12_NC, R_AARCH64_LDST16_ABS_LO12_NC,
+ R_AARCH64_LDST32_ABS_LO12_NC, R_AARCH64_LDST64_ABS_LO12_NC,
+ R_AARCH64_LDST128_ABS_LO12_NC,
R_RV_HI20, R_RV_LO12_I, R_RV_LO12_S, R_RV_BRANCH, R_RV_JAL, R_RV_CALL,
R_WASM_FUNCIDX, R_WASM_TABLEIDX, R_WASM_MEMOFS, R_WASM_TYPEIDX,
} RelocKind;
diff --git a/test/elf/README.md b/test/elf/README.md
@@ -0,0 +1,70 @@
+# test/elf — ELF format roundtrip harness
+
+Data-driven testing for `emit_elf` / `read_elf` (and, eventually,
+`link_emit_image_writer` for ET_DYN executables).
+
+## Layout
+
+```
+unit/ *.c hand-built ObjBuilder roundtrip tests (link libcfree.a)
+cases/ *.c clang-oracle cases: clang -c → cfree-roundtrip → diff
+bad/ *.elf malformed inputs; *.expect carries the panic substring
+exec/ ET_DYN exec tests (empty until link_resolve lands)
+cfree-roundtrip.c read_elf → emit_elf test driver
+normalize.py readelf/objdump output canonicalizer for structural diff
+run.sh top-level driver, invoked by `make test`
+```
+
+## Running
+
+```
+make test
+```
+
+Skipped tests are surfaced and treated as failures by default. To allow
+skips (e.g. on a host without `qemu-aarch64-static`), run:
+
+```
+CFREE_TEST_ALLOW_SKIP=1 make test
+```
+
+## Adding a case
+
+1. Drop `cases/NN_short_name.c` — a tiny self-contained C program (no
+ external libs unless they're listed in the harness `clang -static`
+ step).
+2. Run `make test`. The harness will:
+ - `clang --target=aarch64-linux-gnu -c` your file → `golden.o`
+ - run `cfree-roundtrip golden.o rt.o`
+ - normalize `llvm-readelf -aW` output of both and diff
+ - if `qemu-aarch64-static` is available: link both with clang and
+ compare stdout/stderr/exit code.
+3. If your case exercises a reloc kind cfree doesn't yet support, drop
+ an empty `NN_short_name.xfail` next to the `.c`. The harness expects
+ the case to fail; if it starts passing, remove the `.xfail` (or the
+ harness will fail loudly with `XPASS`).
+
+## Adding a unit test
+
+Drop `unit/<name>.c` that builds an `ObjBuilder` programmatically,
+calls `emit_elf` to a memory writer, then `read_elf`, and asserts shape.
+Exit 0 on success, non-zero on failure. See `unit/smoke.c` for the
+template.
+
+## Adding a negative test
+
+1. Produce a malformed ELF blob in `bad/<name>.elf` (hand-crafted or
+ `objcopy --corrupt`).
+2. Write `bad/<name>.expect` containing a substring that should appear
+ in `cfree-roundtrip`'s stderr after the panic. The harness asserts
+ non-zero exit and the substring presence (and zero signals — a
+ segfault is a hard fail).
+
+## Host requirements
+
+Optional but each unlocks a layer:
+
+- `clang` with `--target=aarch64-linux-gnu` — Layer B (cases).
+- `qemu-aarch64-static` (or `qemu-aarch64`) — behavioral comparison.
+- `llvm-readelf`, `llvm-objdump` — structural diff.
+- `python3` — `normalize.py`.
diff --git a/test/elf/bad/.gitkeep b/test/elf/bad/.gitkeep
diff --git a/test/elf/cases/.gitkeep b/test/elf/cases/.gitkeep
diff --git a/test/elf/cases/01_return42.c b/test/elf/cases/01_return42.c
@@ -0,0 +1,18 @@
+/* Trivial function: no relocations, single .text section.
+ *
+ * Marked .xfail until cfree's data model preserves a few details clang
+ * emits in even the simplest .o:
+ * - STT_NOTYPE on defined symbols (AArch64 mapping symbols `$x`/`$d`)
+ * — currently round-trip as STT_OBJECT because SymKind has no slot
+ * for "defined but typeless".
+ * - Custom sh_type values like SHT_LLVM_ADDRSIG — read_elf collapses
+ * unknown types to SSEM_PROGBITS.
+ * - Section ordering / single-vs-split strtab+shstrtab — clang reuses
+ * one .strtab for both; cfree writes them separately. Cosmetic, but
+ * affects the structural diff.
+ *
+ * Roundtrip is structurally sound (same sections, symbols, and relocs);
+ * the lossy bits are why this case is xfail. Removing the .xfail file
+ * is the regression check once the model is extended. */
+
+int main(void) { return 42; }
diff --git a/test/elf/cfree-roundtrip.c b/test/elf/cfree-roundtrip.c
@@ -0,0 +1,161 @@
+/* cfree-roundtrip: read an ELF object via libcfree's read_elf, then re-emit
+ * via emit_elf. Used by test/elf/run.sh as the structural roundtrip oracle.
+ *
+ * Usage: cfree-roundtrip <in.o> <out.o>
+ *
+ * Behavior: cfree_detect_target on the input bytes selects the Compiler
+ * target; read_elf parses into an ObjBuilder; emit_elf writes the
+ * canonical re-emit to out.o. Diagnostics go to stderr via the libc heap
+ * + stderr diag sink. compiler_panic exits the process with status 2 and
+ * the diagnostic text on stderr — this is the path the negative tests
+ * (test/elf/bad/) exercise.
+ *
+ * Mixes public (<cfree.h>) and internal (src/obj/obj.h, src/core/core.h)
+ * headers — this is a test binary, not a libcfree consumer, so seeing the
+ * internal surface is fine. */
+
+#include <cfree.h>
+#include "core/core.h"
+#include "obj/obj.h"
+
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/* ---- env vtables (libc-backed) ---- */
+
+static void* heap_alloc (CfreeHeap* h, size_t n, size_t a)
+{ (void)h; (void)a; return n ? malloc(n) : NULL; }
+static void* heap_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 heap_free (CfreeHeap* h, void* p, size_t n)
+{ (void)h; (void)n; free(p); }
+static CfreeHeap g_heap = { heap_alloc, heap_realloc, heap_free, NULL };
+
+static void diag_emit(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_emit, NULL, 0, 0 };
+
+/* ---- file I/O helpers ---- */
+
+static int read_file(const char* path, uint8_t** data_out, size_t* len_out)
+{
+ 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 = (uint8_t*)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);
+
+ *data_out = buf;
+ *len_out = n;
+ return 0;
+}
+
+static int write_file(const char* path, const uint8_t* data, size_t len)
+{
+ int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) return -1;
+ size_t off = 0;
+ while (off < len) {
+ ssize_t k = write(fd, data + off, len - off);
+ if (k < 0) { close(fd); return -1; }
+ off += (size_t)k;
+ }
+ close(fd);
+ return 0;
+}
+
+/* ---- main ---- */
+
+int main(int argc, char** argv)
+{
+ if (argc != 3) {
+ fprintf(stderr, "usage: cfree-roundtrip <in.o> <out.o>\n");
+ return 2;
+ }
+ const char* in_path = argv[1];
+ const char* out_path = argv[2];
+
+ uint8_t* in_data = NULL;
+ size_t in_len = 0;
+ if (read_file(in_path, &in_data, &in_len) != 0) {
+ fprintf(stderr, "error: cannot read %s\n", in_path);
+ return 1;
+ }
+
+ CfreeTarget target;
+ if (cfree_detect_target(in_data, in_len, &target) != 0) {
+ fprintf(stderr, "error: %s: not a recognized object file\n", in_path);
+ free(in_data);
+ return 1;
+ }
+
+ CfreeEnv env;
+ env.heap = &g_heap;
+ env.file_io = NULL;
+ env.diag = &g_diag;
+
+ CfreeCompiler* c = cfree_compiler_new(target, &env);
+ if (!c) {
+ fprintf(stderr, "error: cfree_compiler_new failed\n");
+ free(in_data);
+ return 1;
+ }
+
+ /* read_elf and emit_elf are called inside their own panic boundary.
+ * compiler_panic longjmps to c->panic, so we install setjmp here. */
+ if (setjmp(((Compiler*)c)->panic)) {
+ compiler_run_cleanups((Compiler*)c);
+ cfree_compiler_free(c);
+ free(in_data);
+ return 2;
+ }
+
+ ObjBuilder* ob = read_elf((Compiler*)c, in_path, in_data, in_len);
+
+ CfreeWriter* w = cfree_writer_mem(&g_heap);
+ if (!w) {
+ fprintf(stderr, "error: cfree_writer_mem failed\n");
+ obj_free(ob);
+ cfree_compiler_free(c);
+ free(in_data);
+ return 1;
+ }
+
+ emit_elf((Compiler*)c, ob, w);
+
+ size_t out_len = 0;
+ const uint8_t* out_data = cfree_writer_mem_bytes(w, &out_len);
+
+ int rc = write_file(out_path, out_data, out_len);
+ if (rc != 0) fprintf(stderr, "error: cannot write %s\n", out_path);
+
+ cfree_writer_close(w);
+ obj_free(ob);
+ cfree_compiler_free(c);
+ free(in_data);
+ return rc == 0 ? 0 : 1;
+}
diff --git a/test/elf/exec/.gitkeep b/test/elf/exec/.gitkeep
diff --git a/test/elf/exec/01_exit_0.c b/test/elf/exec/01_exit_0.c
@@ -0,0 +1,16 @@
+/* Layer D: minimum-viable exec — _start exits 0.
+ *
+ * No relocations: validates ehdr/phdr/segment plumbing and e_entry. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+void _start(void)
+{
+ sys_exit(0);
+}
diff --git a/test/elf/exec/02_exit_42.c b/test/elf/exec/02_exit_42.c
@@ -0,0 +1,15 @@
+/* Layer D: same as 01 but with a non-zero exit code, to catch
+ * accidental hard-coding of zero somewhere in the e_entry path. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+void _start(void)
+{
+ sys_exit(42);
+}
diff --git a/test/elf/exec/03_call_local.c b/test/elf/exec/03_call_local.c
@@ -0,0 +1,22 @@
+/* Layer D: in-section call to exercise R_AARCH64_CALL26.
+ *
+ * `_start` calls a sibling function in the same .text — clang
+ * lowers that to BL with a R_AARCH64_CALL26 relocation. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+static int __attribute__((noinline)) double_it(int x)
+{
+ return x + x;
+}
+
+void _start(void)
+{
+ sys_exit(double_it(7)); /* exits 14 */
+}
diff --git a/test/elf/exec/04_load_rodata.c b/test/elf/exec/04_load_rodata.c
@@ -0,0 +1,18 @@
+/* Layer D: load a rodata constant via ADRP + ADD + LDR. Exercises
+ * R_AARCH64_ADR_PREL_PG_HI21 + R_AARCH64_ADD_ABS_LO12_NC across
+ * segments (text -> rodata). */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+static const int answer = 17;
+
+void _start(void)
+{
+ sys_exit(answer);
+}
diff --git a/test/elf/exec/05_load_data.c b/test/elf/exec/05_load_data.c
@@ -0,0 +1,18 @@
+/* Layer D: load a writable .data int. Exercises the R+W segment plus
+ * the same ADRP/ADD pair across text -> data. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+static int counter = 11;
+
+void _start(void)
+{
+ counter += 1;
+ sys_exit(counter); /* exits 12 */
+}
diff --git a/test/elf/exec/06_bss.c b/test/elf/exec/06_bss.c
@@ -0,0 +1,18 @@
+/* Layer D: BSS — declare zero-initialised int, write, read, exit.
+ * Tests segment.mem_size > file_size and zero-on-load. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+static int x; /* BSS */
+
+void _start(void)
+{
+ x = 7;
+ sys_exit(x);
+}
diff --git a/test/elf/exec/07_two_tus.c b/test/elf/exec/07_two_tus.c
@@ -0,0 +1,22 @@
+/* Layer D: cross-TU symbol resolution. The harness only feeds one .c
+ * per case, so this exercise lives in a single file with two
+ * independently-emitted TUs joined via __attribute__((section)) on the
+ * defined int. Equivalent to two .c files with `extern int answer` on
+ * one side and `int answer = 42;` on the other — both end up in one
+ * translation unit at -O0 with separate function bodies clang lowers
+ * via PG_HI21+ADD_LO12 to .data. */
+
+static void __attribute__((noreturn)) sys_exit(long code)
+{
+ register long x0 __asm__("x0") = code;
+ register long x8 __asm__("x8") = 93; /* __NR_exit */
+ __asm__ volatile("svc #0" :: "r"(x0), "r"(x8));
+ __builtin_unreachable();
+}
+
+int answer = 42; /* exported global */
+
+void _start(void)
+{
+ sys_exit(answer);
+}
diff --git a/test/elf/normalize.py b/test/elf/normalize.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""Canonicalize llvm-readelf / llvm-objdump output so two ELFs with
+equivalent semantic content compare equal.
+
+Strips/normalizes:
+ - file offsets, virtual addresses (replaced with "<addr>")
+ - section/symbol indices (replaced with "<idx>")
+ - string-table offsets that show up after a "name:" tag
+
+Sorts:
+ - "Symbol table" entries by (binding, name) within each scope
+ - "Relocation section" entries by (section, offset)
+
+Invocation:
+ normalize.py readelf <file> — runs `llvm-readelf -aW`, then normalizes
+ normalize.py objdump <file> — runs `llvm-objdump -drwhW`, then normalizes
+ normalize.py filter — reads stdin, writes normalized to stdout
+"""
+import os
+import re
+import shutil
+import subprocess
+import sys
+
+
+def _which(*names):
+ for n in names:
+ p = shutil.which(n)
+ if p:
+ return p
+ return None
+
+
+def _run(tool_args, file_path):
+ bin_path = _which(*tool_args[0])
+ if not bin_path:
+ sys.stderr.write("normalize.py: cannot find %s\n" % tool_args[0][0])
+ sys.exit(77)
+ res = subprocess.run([bin_path] + tool_args[1] + [file_path],
+ capture_output=True, text=True)
+ sys.stderr.write(res.stderr)
+ return res.stdout
+
+
+# Hex address: 0x[0-9a-f]+ (>=4 digits, to avoid clobbering small numerics like flags).
+_HEX_ADDR = re.compile(r"0x[0-9a-fA-F]{4,}")
+# Bare hex addresses (no 0x) only inside specific column contexts handled below.
+_OFFSET_LINE = re.compile(r"^\s*Offset:\s+\S+\s*$")
+# llvm-readelf section header lines:
+# [Nr] Name Type Address Off Size ES Flg Lk Inf Al
+# Flg can be empty (sections like SHT_STRTAB / SHT_NOBITS-but-not-allocatable),
+# so we parse positionally rather than with a single regex.
+_SHDR_HEADER_RE = re.compile(
+ r"^\s*\[\s*(\d+)\]\s+(\S+)\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(.*)$"
+)
+# llvm-readelf symbol table row:
+# Num: Value Size Type Bind Vis Ndx Name
+_SYM_LINE = re.compile(
+ r"^\s*(\d+):\s+([0-9a-fA-F]+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$"
+)
+# llvm-readelf RELA row:
+# Offset Info Type Symbol's Value Symbol's Name + Addend
+_RELA_LINE = re.compile(
+ r"^\s*([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+(\S+)\s+([0-9a-fA-F]+)\s+(.*)$"
+)
+
+
+# Sections cfree's data model doesn't fully preserve. Drop them from
+# the structural diff entirely so the rest of the comparison is
+# meaningful. Each entry here is paired with a comment in the C
+# implementation noting why; remove from this set when the model is
+# extended.
+_DROP_SHDR_NAMES = {
+ # SHT_LLVM_ADDRSIG (0x6FFF4C03) + SHF_EXCLUDE (0x80000000) — LLVM
+ # address-significance hint; cfree collapses unknown sh_types to
+ # SSEM_PROGBITS and has no SF_EXCLUDE in the SecFlag enum.
+ ".llvm_addrsig",
+}
+
+
+def _normalize_shdr(line):
+ m = _SHDR_HEADER_RE.match(line)
+ if not m:
+ return line
+ nr, name, sh_type, addr, off, size, rest = m.groups()
+ if name in _DROP_SHDR_NAMES:
+ return ""
+ toks = rest.split()
+ # rest is "ES [Flg] Lk Inf Al" — Flg can be empty; treat as 4 or 5 tokens.
+ if len(toks) == 5:
+ es, flg, lk, inf, al = toks
+ elif len(toks) == 4:
+ es, flg, lk, inf, al = toks[0], "", toks[1], toks[2], toks[3]
+ else:
+ return line
+ # Drop nr/addr/off and the link/info indices (they reference other
+ # sections positionally and vary with layout). Section semantics are
+ # captured by name/type/flg/al; link relationships and exact byte
+ # sizes re-emerge from the symtab/rela contents downstream.
+ #
+ # STRTAB size depends on whether the implementation does tail
+ # merging (".eh_frame" stored as a suffix of ".rela.eh_frame", etc.),
+ # which is an optimization, not a contract — strip the size for
+ # STRTAB so equivalent-content tables compare equal.
+ if sh_type == "STRTAB":
+ return ("[<idx>] %s STRTAB es=%s flg=%s al=%s\n" % (name, es, flg, al))
+ return ("[<idx>] %s %s size=%s es=%s flg=%s al=%s\n"
+ % (name, sh_type, size, es, flg, al))
+
+
+def _normalize_sym(line):
+ m = _SYM_LINE.match(line)
+ if not m:
+ return line
+ num, val, size, sym_type, bind, vis, ndx, name = m.groups()
+ # ndx as a numeric is layout-dependent; collapse all numeric ndx to
+ # "DEF" (defined-in-some-section) and keep the special markers
+ # (UND / ABS / COMMON / etc.).
+ if ndx.isdigit():
+ ndx = "DEF"
+ return ("[<idx>] value=<addr> size=%s type=%s bind=%s vis=%s ndx=%s name=%s\n"
+ % (size, sym_type, bind, vis, ndx, name))
+
+
+def _normalize_rela(line):
+ m = _RELA_LINE.match(line)
+ if not m:
+ return line
+ off, info, rtype, sym_val, sym_name = m.groups()
+ return ("offset=%s type=%s sym_value=<addr> sym=%s\n"
+ % (off, rtype, sym_name.strip()))
+
+
+# Lines whose presence is sensitive to layout choices but says nothing
+# semantic: count of headers, where they live, etc. Drop them entirely.
+_DROP_PREFIXES = (
+ " Start of section headers:",
+ " Number of section headers:",
+ " Section header string table index:",
+ "There are ", # "There are N section headers..."
+ "Symbol table '", # "Symbol table '.symtab' contains N entries"
+ "Relocation section '", # "Relocation section '.rela.X' at offset N..."
+)
+
+def _is_segment_mapping(line):
+ return line.lstrip().startswith("None ") or "Segment Sections..." in line
+
+
+def normalize(text):
+ out_blocks = []
+ cur_block = []
+ cur_kind = None # "shdr", "sym", "rela", None
+
+ def flush():
+ nonlocal cur_block, cur_kind
+ # Sort all block kinds; section ordering and symbol ordering are not
+ # semantic. (Relocation sections within a relocation block are
+ # already named, so sorting is fine.)
+ if cur_kind in ("shdr", "sym", "rela"):
+ cur_block.sort()
+ out_blocks.extend(cur_block)
+ cur_block = []
+ cur_kind = None
+
+ for raw in text.splitlines(keepends=True):
+ line = raw
+
+ # Block-start markers come first — even if their text matches
+ # _DROP_PREFIXES, they still need to set cur_kind. We replace
+ # the line itself with a stable canonical heading so the body
+ # can be diffed without the count/offset suffix.
+ if line.startswith("Symbol table"):
+ flush()
+ cur_kind = "sym"
+ out_blocks.append("Symbol table:\n")
+ continue
+ if line.startswith("Relocation section"):
+ flush()
+ cur_kind = "rela"
+ out_blocks.append("Relocation section:\n")
+ continue
+ if "Section Headers:" in line:
+ flush()
+ cur_kind = "shdr"
+ out_blocks.append(line)
+ continue
+
+ if any(line.startswith(p) for p in _DROP_PREFIXES):
+ continue
+
+ # Section-to-segment mapping line: sort section names so file
+ # ordering doesn't show through; drop any sections in the
+ # known-not-preserved set.
+ if cur_kind is None and _is_segment_mapping(line.rstrip("\n")):
+ stripped = line.rstrip("\n")
+ if "None" in stripped:
+ head, _, tail = stripped.partition("None")
+ names = sorted(n for n in tail.split() if n not in _DROP_SHDR_NAMES)
+ line = head + "None " + " ".join(names) + "\n"
+ if not line.strip():
+ flush()
+ out_blocks.append(line)
+ continue
+
+ if cur_kind == "shdr":
+ cur_block.append(_normalize_shdr(line))
+ continue
+ if cur_kind == "sym":
+ cur_block.append(_normalize_sym(line))
+ continue
+ if cur_kind == "rela":
+ cur_block.append(_normalize_rela(line))
+ continue
+
+ # Default: scrub addresses outside section bodies too.
+ line = _HEX_ADDR.sub("<addr>", line)
+ out_blocks.append(line)
+
+ flush()
+ return "".join(out_blocks)
+
+
+def main(argv):
+ if len(argv) < 2:
+ sys.stderr.write(__doc__)
+ return 2
+ cmd = argv[1]
+ if cmd == "readelf":
+ if len(argv) != 3: return 2
+ text = _run((["llvm-readelf", "readelf"], ["-aW"]), argv[2])
+ elif cmd == "objdump":
+ if len(argv) != 3: return 2
+ text = _run((["llvm-objdump", "objdump"], ["-drwhW"]), argv[2])
+ elif cmd == "filter":
+ text = sys.stdin.read()
+ else:
+ sys.stderr.write(__doc__)
+ return 2
+ sys.stdout.write(normalize(text))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/test/elf/run.sh b/test/elf/run.sh
@@ -0,0 +1,418 @@
+#!/usr/bin/env bash
+# test/elf/run.sh — top-level driver for ELF roundtrip tests.
+#
+# Layers (each runs only when its prerequisites are present):
+#
+# A. test/elf/unit/*.c hand-built ObjBuilder roundtrip tests.
+# Each .c links against build/libcfree.a, runs,
+# and exits 0 on success.
+# B. test/elf/cases/*.c clang-oracle behavioral tests:
+# clang -c case.c -> golden.o
+# cfree-roundtrip golden.o -> rt.o
+# clang ... -> golden.exe / rt.exe
+# diff stdout/stderr/exit + structural diff.
+# C. test/elf/bad/*.elf malformed inputs; expect cfree-roundtrip
+# to fail with a substring from <name>.expect.
+#
+# Tools detected: clang, llvm-readelf, llvm-objdump, qemu-aarch64-static,
+# python3. Missing tools cause the dependent layer to be skipped, not
+# failed. Set CFREE_TEST_ALLOW_SKIP=1 to allow the harness to exit 0 with
+# skips; otherwise any skip makes the run fail (so CI catches a silently
+# degraded run).
+
+set -u
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+TEST_DIR="$ROOT/test/elf"
+BUILD_DIR="$ROOT/build/test"
+LIB_AR="$ROOT/build/libcfree.a"
+NORMALIZE="$TEST_DIR/normalize.py"
+
+mkdir -p "$BUILD_DIR"
+
+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=0
+have_llvm_readelf=0
+have_llvm_objdump=0
+have_qemu=0
+have_podman=0
+have_python3=0
+
+command -v clang >/dev/null 2>&1 && have_clang=1
+command -v llvm-readelf >/dev/null 2>&1 && have_llvm_readelf=1
+command -v readelf >/dev/null 2>&1 && have_llvm_readelf=1
+command -v llvm-objdump >/dev/null 2>&1 && have_llvm_objdump=1
+command -v objdump >/dev/null 2>&1 && have_llvm_objdump=1
+command -v qemu-aarch64-static >/dev/null 2>&1 && have_qemu=1
+command -v qemu-aarch64 >/dev/null 2>&1 && have_qemu=1
+command -v python3 >/dev/null 2>&1 && have_python3=1
+command -v podman >/dev/null 2>&1 && have_podman=1
+
+QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || echo)"
+READELF_BIN="$(command -v llvm-readelf 2>/dev/null || command -v readelf 2>/dev/null || echo)"
+
+# AArch64 binary runner. Layer B (clang-oracle behavioral diff) and Layer D
+# (cfree ld behavioral diff) need to execute aarch64 ELF binaries. On Linux
+# hosts qemu-aarch64-static does this directly; on macOS we shell out to
+# podman, which transparently runs the binary in a Linux ARM64 container
+# (native on Apple Silicon, qemu-emulated on x86 macs). The image is
+# selected via $RUN_AARCH64_IMAGE (default alpine:latest); a podman machine
+# must already be running.
+#
+# Usage: run_aarch64 <exe-abs-path> <stdout-file> <stderr-file>
+# Sets: RUN_RC exit code returned by the binary (or runner failure code)
+RUN_AARCH64_IMAGE="${RUN_AARCH64_IMAGE:-alpine:latest}"
+have_runner=0
+if [ $have_qemu -eq 1 ] || [ $have_podman -eq 1 ]; then
+ have_runner=1
+fi
+
+run_aarch64() {
+ local exe="$1" out="$2" err="$3"
+ if [ $have_qemu -eq 1 ]; then
+ "$QEMU_BIN" "$exe" > "$out" 2> "$err"
+ RUN_RC=$?
+ return
+ fi
+ if [ $have_podman -eq 1 ]; then
+ local dir base
+ dir="$(cd "$(dirname "$exe")" && pwd)"
+ base="$(basename "$exe")"
+ # --net=none keeps the runs hermetic; -w /work + ./binary so that
+ # static-nostdlib binaries do not depend on $PATH lookup.
+ podman run --rm --platform linux/arm64 --net=none \
+ -v "$dir":/work:Z -w /work \
+ "$RUN_AARCH64_IMAGE" "./$base" > "$out" 2> "$err"
+ RUN_RC=$?
+ return
+ fi
+ RUN_RC=127
+}
+
+runner_label() {
+ if [ $have_qemu -eq 1 ]; then echo "qemu ($QEMU_BIN)"; return; fi
+ if [ $have_podman -eq 1 ]; then echo "podman ($RUN_AARCH64_IMAGE)"; return; fi
+ echo "none"
+}
+
+# ----- build cfree-roundtrip --------------------------------------------
+
+ROUNDTRIP_BIN="$BUILD_DIR/cfree-roundtrip"
+roundtrip_ok=0
+
+build_roundtrip() {
+ if [ ! -f "$LIB_AR" ]; then
+ return 1
+ fi
+ local cc="${CC:-clang}"
+ local sysroot
+ sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)"
+ local sysroot_flag=""
+ [ -n "$sysroot" ] && sysroot_flag="-isysroot $sysroot"
+ # shellcheck disable=SC2086
+ "$cc" -std=c11 -Wall -Wextra -Werror $sysroot_flag \
+ -I"$ROOT/include" -I"$ROOT/src" \
+ "$TEST_DIR/cfree-roundtrip.c" "$LIB_AR" \
+ -o "$ROUNDTRIP_BIN" 2> "$BUILD_DIR/cfree-roundtrip.build.log"
+}
+
+if build_roundtrip; then
+ roundtrip_ok=1
+fi
+
+# ----- header summary ----------------------------------------------------
+
+printf 'cfree ELF test harness\n'
+printf ' clang: %s\n' "$([ $have_clang -eq 1 ] && echo yes || echo no)"
+printf ' llvm-readelf: %s\n' "$([ $have_llvm_readelf -eq 1 ] && echo yes || echo no)"
+printf ' llvm-objdump: %s\n' "$([ $have_llvm_objdump -eq 1 ] && echo yes || echo no)"
+printf ' qemu-aarch64: %s\n' "$([ $have_qemu -eq 1 ] && echo yes || echo no)"
+printf ' podman: %s\n' "$([ $have_podman -eq 1 ] && echo yes || echo no)"
+printf ' aarch64 runner: %s\n' "$(runner_label)"
+printf ' python3: %s\n' "$([ $have_python3 -eq 1 ] && echo yes || echo no)"
+printf ' cfree-roundtrip: %s\n' "$([ $roundtrip_ok -eq 1 ] && echo built || echo "BUILD FAILED — see $BUILD_DIR/cfree-roundtrip.build.log")"
+printf '\n'
+
+# ----- Layer A: unit/*.c -------------------------------------------------
+
+printf 'Layer A — unit tests\n'
+shopt -s nullglob
+unit_srcs=( "$TEST_DIR"/unit/*.c )
+if [ ${#unit_srcs[@]} -eq 0 ]; then
+ printf ' (no unit tests yet)\n'
+else
+ for src in "${unit_srcs[@]}"; do
+ name="unit/$(basename "$src" .c)"
+ if [ ! -f "$LIB_AR" ]; then
+ note_skip "$name" "build/libcfree.a missing"
+ continue
+ fi
+ bin="$BUILD_DIR/$(basename "$src" .c)"
+ cc="${CC:-clang}"
+ sysroot="$(xcrun --show-sdk-path 2>/dev/null || true)"
+ sysroot_flag=""
+ [ -n "$sysroot" ] && sysroot_flag="-isysroot $sysroot"
+ # shellcheck disable=SC2086
+ if ! "$cc" -std=c11 -Wall -Wextra -Werror $sysroot_flag \
+ -I"$ROOT/include" -I"$ROOT/src" \
+ "$src" "$LIB_AR" -o "$bin" 2> "$BUILD_DIR/$(basename "$src" .c).build.log"; then
+ note_fail "$name"
+ continue
+ fi
+ if "$bin" > "$BUILD_DIR/$(basename "$src" .c).out" 2>&1; then
+ note_pass "$name"
+ else
+ note_fail "$name"
+ sed 's/^/ | /' "$BUILD_DIR/$(basename "$src" .c).out"
+ fi
+ done
+fi
+printf '\n'
+
+# ----- Layer B: cases/*.c ------------------------------------------------
+
+printf 'Layer B — clang-oracle cases\n'
+case_srcs=( "$TEST_DIR"/cases/*.c )
+if [ ${#case_srcs[@]} -eq 0 ]; then
+ printf ' (no cases yet)\n'
+else
+ for src in "${case_srcs[@]}"; do
+ name="cases/$(basename "$src" .c)"
+ # Per-case skip reasons:
+ if [ $roundtrip_ok -ne 1 ]; then note_skip "$name" "cfree-roundtrip not built"; continue; fi
+ if [ $have_clang -ne 1 ]; then note_skip "$name" "clang missing"; continue; fi
+ if [ $have_llvm_readelf -ne 1 ]; then note_skip "$name" "llvm-readelf missing"; continue; fi
+ if [ $have_python3 -ne 1 ]; then note_skip "$name" "python3 missing"; continue; fi
+
+ stem="$(basename "$src" .c)"
+ wd="$BUILD_DIR/$stem"
+ mkdir -p "$wd"
+
+ # Honor an optional ".xfail" sentinel beside the .c — for cases that
+ # exercise reloc kinds not yet supported. xfail-cases must FAIL until
+ # the kind lands; if they start passing, that is a hard failure.
+ xfail=0
+ [ -f "${src%.c}.xfail" ] && xfail=1
+
+ if ! clang --target=aarch64-linux-gnu -c -O0 "$src" -o "$wd/golden.o" \
+ 2> "$wd/clang.log"; then
+ note_skip "$name" "clang -c failed (cross-compile not configured?)"
+ continue
+ fi
+
+ rt_ok=1
+ "$ROUNDTRIP_BIN" "$wd/golden.o" "$wd/rt.o" 2> "$wd/roundtrip.log" || rt_ok=0
+
+ if [ $rt_ok -ne 1 ]; then
+ if [ $xfail -eq 1 ]; then
+ note_pass "$name (xfail: roundtrip rejected)"
+ else
+ note_fail "$name (roundtrip failed)"
+ sed 's/^/ | /' "$wd/roundtrip.log"
+ fi
+ continue
+ fi
+
+ # Structural diff: readelf normalized.
+ python3 "$NORMALIZE" readelf "$wd/golden.o" > "$wd/golden.readelf" 2> /dev/null || true
+ python3 "$NORMALIZE" readelf "$wd/rt.o" > "$wd/rt.readelf" 2> /dev/null || true
+ if ! diff -u "$wd/golden.readelf" "$wd/rt.readelf" > "$wd/readelf.diff"; then
+ if [ $xfail -eq 1 ]; then
+ note_pass "$name (xfail: structural diff differs)"
+ continue
+ fi
+ note_fail "$name (readelf diff)"
+ head -40 "$wd/readelf.diff" | sed 's/^/ | /'
+ continue
+ fi
+
+ # Behavioral diff requires an aarch64 runner (qemu or podman).
+ if [ $have_runner -eq 1 ]; then
+ if ! clang --target=aarch64-linux-gnu -static "$wd/golden.o" -o "$wd/golden.exe" \
+ 2> "$wd/link_golden.log"; then
+ note_skip "$name (run)" "linking golden.exe failed"
+ continue
+ fi
+ if ! clang --target=aarch64-linux-gnu -static "$wd/rt.o" -o "$wd/rt.exe" \
+ 2> "$wd/link_rt.log"; then
+ note_fail "$name (run): linking rt.exe failed"
+ continue
+ fi
+ run_aarch64 "$wd/golden.exe" "$wd/golden.out" "$wd/golden.err"; ge=$RUN_RC
+ run_aarch64 "$wd/rt.exe" "$wd/rt.out" "$wd/rt.err"; re=$RUN_RC
+ cat "$wd/golden.err" >> "$wd/golden.out"
+ cat "$wd/rt.err" >> "$wd/rt.out"
+ echo $ge >> "$wd/golden.out"
+ echo $re >> "$wd/rt.out"
+ if ! diff -u "$wd/golden.out" "$wd/rt.out" > "$wd/run.diff"; then
+ if [ $xfail -eq 1 ]; then
+ note_pass "$name (xfail: run output differs)"
+ continue
+ fi
+ note_fail "$name (run output)"
+ head -40 "$wd/run.diff" | sed 's/^/ | /'
+ continue
+ fi
+ fi
+
+ if [ $xfail -eq 1 ]; then
+ note_fail "$name (XPASS — case marked xfail but everything matched; remove .xfail)"
+ else
+ note_pass "$name"
+ fi
+ done
+fi
+printf '\n'
+
+# ----- Layer C: bad/*.elf ------------------------------------------------
+
+printf 'Layer C — negative read_elf inputs\n'
+bad_files=( "$TEST_DIR"/bad/*.elf )
+if [ ${#bad_files[@]} -eq 0 ]; then
+ printf ' (no bad inputs yet)\n'
+else
+ for blob in "${bad_files[@]}"; do
+ name="bad/$(basename "$blob" .elf)"
+ if [ $roundtrip_ok -ne 1 ]; then note_skip "$name" "cfree-roundtrip not built"; continue; fi
+
+ stem="$(basename "$blob" .elf)"
+ wd="$BUILD_DIR/bad_$stem"
+ mkdir -p "$wd"
+
+ rc=0
+ "$ROUNDTRIP_BIN" "$blob" "$wd/out.o" > "$wd/stdout.log" 2> "$wd/stderr.log" || rc=$?
+
+ if [ $rc -eq 0 ]; then
+ note_fail "$name (expected nonzero exit, got 0)"
+ continue
+ fi
+ if [ $rc -ge 128 ]; then
+ note_fail "$name (terminated by signal $((rc - 128)) — segfault?)"
+ continue
+ fi
+
+ expect_file="${blob%.elf}.expect"
+ if [ ! -f "$expect_file" ]; then
+ note_fail "$name (missing $expect_file)"
+ continue
+ fi
+ expect="$(cat "$expect_file")"
+ if grep -qF -- "$expect" "$wd/stderr.log"; then
+ note_pass "$name"
+ else
+ note_fail "$name (stderr did not contain: $expect)"
+ sed 's/^/ | /' "$wd/stderr.log"
+ fi
+ done
+fi
+printf '\n'
+
+# ----- Layer D: exec/*.c — exe behavioral comparison --------------------
+#
+# clang -c case.c -> golden.o
+# clang -static -nostdlib golden.o -> golden.exe (lld)
+# cfree ld -o cfree.exe golden.o
+# qemu-aarch64 golden.exe / cfree.exe; diff stdout/stderr/exit.
+
+printf 'Layer D — exe behavioral comparison\n'
+exec_srcs=( "$TEST_DIR"/exec/[0-9]*.c )
+CFREE_BIN="${CFREE:-$ROOT/build/cfree}"
+if [ ${#exec_srcs[@]} -eq 0 ]; then
+ printf ' (no exec cases yet)\n'
+else
+ for src in "${exec_srcs[@]}"; do
+ name="exec/$(basename "$src" .c)"
+ if [ $have_clang -ne 1 ]; then note_skip "$name" "clang missing"; continue; fi
+ if [ $have_runner -ne 1 ]; then note_skip "$name" "no aarch64 runner (qemu/podman)"; continue; fi
+ if [ ! -x "$CFREE_BIN" ]; then note_skip "$name" "cfree binary not built"; continue; fi
+
+ stem="$(basename "$src" .c)"
+ wd="$BUILD_DIR/exec_$stem"
+ mkdir -p "$wd"
+
+ xfail=0
+ [ -f "${src%.c}.xfail" ] && xfail=1
+
+ if ! clang --target=aarch64-linux-gnu -c -O0 -ffreestanding -fno-pic \
+ "$src" -o "$wd/case.o" 2> "$wd/clang_c.log"; then
+ note_skip "$name" "clang -c failed (cross-compile not configured?)"
+ continue
+ fi
+
+ if ! clang --target=aarch64-linux-gnu -fuse-ld=lld -static -nostdlib \
+ "$wd/case.o" -o "$wd/golden.exe" 2> "$wd/link_golden.log"; then
+ note_skip "$name" "clang/lld link of golden.exe failed"
+ continue
+ fi
+
+ if ! "$CFREE_BIN" ld -o "$wd/cfree.exe" "$wd/case.o" \
+ 2> "$wd/link_cfree.log"; then
+ if [ $xfail -eq 1 ]; then
+ note_pass "$name (xfail: cfree ld rejected)"
+ else
+ note_fail "$name (cfree ld failed)"
+ sed 's/^/ | /' "$wd/link_cfree.log"
+ fi
+ continue
+ fi
+
+ chmod +x "$wd/cfree.exe" 2>/dev/null || true
+
+ run_aarch64 "$wd/golden.exe" "$wd/golden.out" "$wd/golden.err"; ge=$RUN_RC
+ run_aarch64 "$wd/cfree.exe" "$wd/cfree.out" "$wd/cfree.err"; ce=$RUN_RC
+ printf 'rc=%s\n' "$ge" >> "$wd/golden.out"
+ printf 'rc=%s\n' "$ce" >> "$wd/cfree.out"
+
+ if ! diff -u "$wd/golden.out" "$wd/cfree.out" > "$wd/run.diff" \
+ || ! diff -u "$wd/golden.err" "$wd/cfree.err" >> "$wd/run.diff"; then
+ if [ $xfail -eq 1 ]; then
+ note_pass "$name (xfail: run output differs)"
+ else
+ note_fail "$name (run output differs)"
+ head -40 "$wd/run.diff" | sed 's/^/ | /'
+ fi
+ continue
+ fi
+
+ if [ $xfail -eq 1 ]; then
+ note_fail "$name (XPASS — case marked xfail but everything matched; remove .xfail)"
+ else
+ note_pass "$name"
+ fi
+ done
+fi
+printf '\n'
+
+# ----- summary -----------------------------------------------------------
+
+printf 'Summary: %s passed, %s failed, %s skipped\n' \
+ "$(color_grn $PASS)" \
+ "$([ $FAIL -eq 0 ] && echo $FAIL || color_red $FAIL)" \
+ "$([ $SKIP -eq 0 ] && echo $SKIP || color_yel $SKIP)"
+
+if [ $FAIL -gt 0 ]; then
+ printf 'Failures:\n'
+ for n in "${FAIL_NAMES[@]}"; do printf ' - %s\n' "$n"; done
+ exit 1
+fi
+if [ $SKIP -gt 0 ] && [ "${CFREE_TEST_ALLOW_SKIP:-}" != "1" ]; then
+ printf 'Skips treated as failures (set CFREE_TEST_ALLOW_SKIP=1 to allow):\n'
+ for n in "${SKIP_NAMES[@]}"; do printf ' - %s\n' "$n"; done
+ exit 1
+fi
+exit 0
diff --git a/test/elf/unit/.gitkeep b/test/elf/unit/.gitkeep
diff --git a/test/elf/unit/smoke.c b/test/elf/unit/smoke.c
@@ -0,0 +1,284 @@
+/* Hand-built ObjBuilder roundtrip test.
+ *
+ * Builds a tiny AArch64-Linux ELF in memory: one .text section with two
+ * AArch64 instructions, one .data section with an R_ABS64 relocation
+ * against an external symbol, then runs:
+ *
+ * emit_elf(c, ob, mem_writer)
+ * read_elf(c, "smoke", bytes, len)
+ *
+ * and checks that the readback produces the same shape (modulo
+ * synthesized STT_SECTION symbols and section ordering — the equivalence
+ * the read_elf comment in src/obj/elf_read.c documents).
+ *
+ * Exit 0 = pass; non-zero = fail (with a one-line stderr explanation). */
+
+#include <cfree.h>
+#include "core/core.h"
+#include "core/pool.h"
+#include "obj/obj.h"
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ---- env ---- */
+
+static void* heap_alloc (CfreeHeap* h, size_t n, size_t a)
+{ (void)h; (void)a; return n ? malloc(n) : NULL; }
+static void* heap_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 heap_free (CfreeHeap* h, void* p, size_t n)
+{ (void)h; (void)n; free(p); }
+static CfreeHeap g_heap = { heap_alloc, heap_realloc, heap_free, NULL };
+
+static void diag_emit(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_emit, NULL, 0, 0 };
+
+/* ---- assertion helpers ---- */
+
+static int g_failures;
+#define CHECK(cond, ...) do { \
+ if (!(cond)) { \
+ fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
+ fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); \
+ g_failures++; \
+ } \
+} while (0)
+
+/* ---- input fixture ---- */
+
+/* mov w0, #42 ; ret — AArch64, little-endian. */
+static const uint8_t TEXT_BYTES[8] = {
+ 0x40, 0x05, 0x80, 0x52,
+ 0xc0, 0x03, 0x5f, 0xd6,
+};
+
+static ObjBuilder* build_input(Compiler* c)
+{
+ ObjBuilder* ob = obj_new(c);
+ Pool* p = c->global;
+
+ Sym text = pool_intern_cstr(p, ".text");
+ Sym data = pool_intern_cstr(p, ".data");
+ Sym main = pool_intern_cstr(p, "main");
+ Sym foo = pool_intern_cstr(p, "foo");
+
+ ObjSecId sec_text = obj_section(ob, text, SEC_TEXT,
+ SF_ALLOC | SF_EXEC, 4);
+ ObjSecId sec_data = obj_section(ob, data, SEC_DATA,
+ SF_ALLOC | SF_WRITE, 8);
+
+ obj_write(ob, sec_text, TEXT_BYTES, sizeof TEXT_BYTES);
+
+ static const uint8_t zero8[8] = {0};
+ obj_write(ob, sec_data, zero8, sizeof zero8);
+
+ obj_symbol(ob, main, SB_GLOBAL, SK_FUNC, sec_text, 0, sizeof TEXT_BYTES);
+ ObjSymId sym_foo = obj_symbol(ob, foo, SB_GLOBAL, SK_UNDEF,
+ OBJ_SEC_NONE, 0, 0);
+
+ obj_reloc(ob, sec_data, 0, R_ABS64, sym_foo, 0);
+
+ obj_finalize(ob);
+ return ob;
+}
+
+/* ---- shape inspection on read_elf output ---- */
+
+/* Pool strings are NOT NUL-terminated (pool_intern stores raw bytes), so
+ * compare by length + memcmp rather than strcmp. */
+static int sym_eq_str(Pool* p, Sym s, const char* want)
+{
+ size_t len;
+ const char* got = pool_str(p, s, &len);
+ size_t wlen = 0;
+ while (want[wlen]) ++wlen;
+ return got && len == wlen && memcmp(got, want, len) == 0;
+}
+
+static const Section* find_section_named(const ObjBuilder* ob, Pool* p,
+ const char* want)
+{
+ u32 n = obj_section_count(ob);
+ for (u32 i = 1; i < n; ++i) {
+ const Section* s = obj_section_get(ob, i);
+ if (sym_eq_str(p, s->name, want)) return s;
+ }
+ return NULL;
+}
+
+static ObjSymId find_sym_named(const ObjBuilder* ob, Pool* p, const char* want)
+{
+ ObjSymIter* it = obj_symiter_new(ob);
+ ObjSymEntry e;
+ ObjSymId found = OBJ_SYM_NONE;
+ while (obj_symiter_next(it, &e)) {
+ if (sym_eq_str(p, e.sym->name, want)) { found = e.id; break; }
+ }
+ obj_symiter_free(it);
+ return found;
+}
+
+static void verify_shape(const ObjBuilder* ob, Pool* p)
+{
+ const Section* text = find_section_named(ob, p, ".text");
+ const Section* data = find_section_named(ob, p, ".data");
+
+ CHECK(text != NULL, ".text not present after roundtrip");
+ CHECK(data != NULL, ".data not present after roundtrip");
+ if (text) {
+ CHECK(text->kind == SEC_TEXT, ".text kind is %u, want %u",
+ text->kind, SEC_TEXT);
+ CHECK((text->flags & SF_EXEC) != 0, ".text missing SF_EXEC");
+ CHECK((text->flags & SF_ALLOC) != 0, ".text missing SF_ALLOC");
+ CHECK(text->bytes.total == sizeof TEXT_BYTES,
+ ".text size mismatch: got %u, want %zu",
+ text->bytes.total, sizeof TEXT_BYTES);
+ uint8_t flat[8] = {0};
+ if (text->bytes.total == sizeof TEXT_BYTES) {
+ buf_flatten(&text->bytes, flat);
+ CHECK(memcmp(flat, TEXT_BYTES, sizeof TEXT_BYTES) == 0,
+ ".text bytes do not match input");
+ }
+ }
+ if (data) {
+ CHECK(data->kind == SEC_DATA, ".data kind is %u", data->kind);
+ CHECK((data->flags & SF_WRITE) != 0, ".data missing SF_WRITE");
+ }
+
+ /* Symbols. main: defined function in .text. foo: undefined external. */
+ ObjSymId sym_main = find_sym_named(ob, p, "main");
+ ObjSymId sym_foo = find_sym_named(ob, p, "foo");
+ CHECK(sym_main != OBJ_SYM_NONE, "missing 'main' symbol");
+ CHECK(sym_foo != OBJ_SYM_NONE, "missing 'foo' symbol");
+ if (sym_main) {
+ const ObjSym* s = obj_symbol_get(ob, sym_main);
+ CHECK(s->bind == SB_GLOBAL, "main bind=%u", s->bind);
+ CHECK(s->kind == SK_FUNC, "main kind=%u", s->kind);
+ }
+ if (sym_foo) {
+ const ObjSym* s = obj_symbol_get(ob, sym_foo);
+ CHECK(s->bind == SB_GLOBAL, "foo bind=%u", s->bind);
+ CHECK(s->kind == SK_UNDEF, "foo kind=%u (want SK_UNDEF=%u)",
+ s->kind, SK_UNDEF);
+ CHECK(s->section_id == OBJ_SEC_NONE, "foo not undefined");
+ }
+
+ /* Relocation: a single R_ABS64 against 'foo' in .data, offset 0. */
+ if (data) {
+ /* Find data's section id. */
+ ObjSecId data_id = OBJ_SEC_NONE;
+ u32 n = obj_section_count(ob);
+ for (u32 i = 1; i < n; ++i) {
+ if (obj_section_get(ob, i) == data) { data_id = i; break; }
+ }
+ CHECK(data_id != OBJ_SEC_NONE, "could not locate .data id");
+
+ u32 nr = obj_reloc_count(ob, data_id);
+ CHECK(nr == 1, ".data reloc count = %u, want 1", nr);
+
+ /* obj_relocs returns the start of the flat reloc array; callers
+ * filter by section_id. Total count = sum across sections. */
+ const Reloc* all = obj_relocs(ob, data_id);
+ const Reloc* found = NULL;
+ u32 total = 0;
+ for (u32 j = 0; j < obj_section_count(ob); ++j)
+ total += obj_reloc_count(ob, j);
+ for (u32 i = 0; i < total; ++i) {
+ if (all[i].section_id == data_id) { found = &all[i]; break; }
+ }
+ CHECK(found != NULL, "no reloc on .data");
+ if (found) {
+ CHECK(found->kind == R_ABS64, ".data reloc kind=%u (want R_ABS64=%u)",
+ found->kind, R_ABS64);
+ CHECK(found->offset == 0, ".data reloc offset=%u", found->offset);
+ CHECK(found->addend == 0, ".data reloc addend=%lld",
+ (long long)found->addend);
+ const ObjSym* tsym = obj_symbol_get(ob, found->sym);
+ if (tsym) {
+ size_t l; const char* nm = pool_str(p, tsym->name, &l);
+ CHECK(sym_eq_str(p, tsym->name, "foo"),
+ "reloc target name = %.*s",
+ (int)l, nm ? nm : "(null)");
+ }
+ }
+ }
+}
+
+/* ---- main ---- */
+
+int main(void)
+{
+ CfreeTarget target;
+ memset(&target, 0, sizeof target);
+ target.arch = CFREE_ARCH_ARM_64;
+ target.os = CFREE_OS_LINUX;
+ target.obj = CFREE_OBJ_ELF;
+ target.ptr_size = 8;
+ target.ptr_align = 8;
+ target.big_endian = 0;
+
+ CfreeEnv env;
+ env.heap = &g_heap;
+ env.file_io = NULL;
+ env.diag = &g_diag;
+
+ CfreeCompiler* cc = cfree_compiler_new(target, &env);
+ if (!cc) { fprintf(stderr, "FAIL: cfree_compiler_new\n"); return 1; }
+ Compiler* c = (Compiler*)cc;
+
+ if (setjmp(c->panic)) {
+ compiler_run_cleanups(c);
+ cfree_compiler_free(cc);
+ fprintf(stderr, "FAIL: compiler_panic during roundtrip\n");
+ return 1;
+ }
+
+ /* Build, emit, read back, inspect. */
+ ObjBuilder* in = build_input(c);
+
+ CfreeWriter* w = cfree_writer_mem(&g_heap);
+ emit_elf(c, in, w);
+ size_t out_len = 0;
+ const uint8_t* out_data = cfree_writer_mem_bytes(w, &out_len);
+
+ /* Sanity: ELF magic. */
+ CHECK(out_len >= 64, "emit_elf produced too few bytes (%zu)", out_len);
+ CHECK(out_len >= 4 &&
+ out_data[0] == 0x7f && out_data[1] == 'E' &&
+ out_data[2] == 'L' && out_data[3] == 'F',
+ "emit_elf output missing ELF magic");
+
+ /* Round-trip: copy to private buffer first since the mem writer's
+ * storage is freed on close. */
+ uint8_t* roundtrip = (uint8_t*)malloc(out_len ? out_len : 1);
+ memcpy(roundtrip, out_data, out_len);
+ cfree_writer_close(w);
+
+ ObjBuilder* back = read_elf(c, "smoke", roundtrip, out_len);
+ CHECK(back != NULL, "read_elf returned NULL");
+ if (back) verify_shape(back, c->global);
+
+ if (back) obj_free(back);
+ free(roundtrip);
+ obj_free(in);
+ cfree_compiler_free(cc);
+
+ if (g_failures) {
+ fprintf(stderr, "%d failure(s)\n", g_failures);
+ return 1;
+ }
+ fputs("smoke: OK\n", stderr);
+ return 0;
+}