kit

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

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:
Asrc/obj/elf.h | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/obj/elf_emit.c | 613+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/obj/elf_read.c | 426+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/obj/elf_reloc_aarch64.c | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/obj/obj.c | 419+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/obj/obj.h | 7+++++++
Atest/elf/README.md | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/elf/bad/.gitkeep | 0
Atest/elf/cases/.gitkeep | 0
Atest/elf/cases/01_return42.c | 18++++++++++++++++++
Atest/elf/cfree-roundtrip.c | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/elf/exec/.gitkeep | 0
Atest/elf/exec/01_exit_0.c | 16++++++++++++++++
Atest/elf/exec/02_exit_42.c | 15+++++++++++++++
Atest/elf/exec/03_call_local.c | 22++++++++++++++++++++++
Atest/elf/exec/04_load_rodata.c | 18++++++++++++++++++
Atest/elf/exec/05_load_data.c | 18++++++++++++++++++
Atest/elf/exec/06_bss.c | 18++++++++++++++++++
Atest/elf/exec/07_two_tus.c | 22++++++++++++++++++++++
Atest/elf/normalize.py | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/elf/run.sh | 418+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/elf/unit/.gitkeep | 0
Atest/elf/unit/smoke.c | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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; +}