kit

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

kit-roundtrip-coff.c (47228B)


      1 /* PE/COFF round-trip harness — peer of test/elf/unit/smoke.c and
      2  * the Mach-O unit tests.  Each test_*() builds an ObjBuilder, emits via
      3  * emit_coff into a memory writer, reads back via read_coff, and
      4  * asserts:
      5  *
      6  *   1. Structural equivalence — sections by name, symbols by name,
      7  *      relocations by (section, offset, kind, target-sym-name).
      8  *      Section-symbol synthesis is honored as a known asymmetry
      9  *      (see test/coff/CORPUS.md §10).
     10  *   2. Byte stability — re-emitting the readback ObjBuilder produces
     11  *      the same bytes as the first emit (memcmp).
     12  *
     13  * Mixes public (<kit/core.h>, <kit/object.h>) and internal
     14  * (src/obj/obj.h, src/core/core.h) surfaces.  Compiled with -Isrc
     15  * by mk/test.mk.  Not a libkit consumer in the usual sense — a
     16  * test binary that pokes the same private headers the writer /
     17  * reader use. */
     18 
     19 #include <kit/core.h>
     20 #include <kit/object.h>
     21 #include <setjmp.h>
     22 #include <stdarg.h>
     23 #include <stdio.h>
     24 #include <stdlib.h>
     25 #include <string.h>
     26 
     27 #include "core/core.h"
     28 #include "core/pool.h"
     29 #include "obj/obj.h"
     30 
     31 /* ---- env vtables --------------------------------------------------- */
     32 
     33 static void* heap_alloc(KitHeap* h, size_t n, size_t a) {
     34   (void)h;
     35   (void)a;
     36   return n ? malloc(n) : NULL;
     37 }
     38 static void* heap_realloc(KitHeap* h, void* p, size_t o, size_t n, size_t a) {
     39   (void)h;
     40   (void)o;
     41   (void)a;
     42   return realloc(p, n);
     43 }
     44 static void heap_free(KitHeap* h, void* p, size_t n) {
     45   (void)h;
     46   (void)n;
     47   free(p);
     48 }
     49 static KitHeap g_heap = {heap_alloc, heap_realloc, heap_free, NULL};
     50 
     51 static void diag_emit(KitDiagSink* s, KitDiagKind k, KitSrcLoc loc,
     52                       const char* fmt, va_list ap) {
     53   static const char* names[] = {"note", "warning", "error", "fatal"};
     54   (void)s;
     55   (void)loc;
     56   fprintf(stderr, "%s: ", names[k]);
     57   vfprintf(stderr, fmt, ap);
     58   fputc('\n', stderr);
     59 }
     60 static KitDiagSink g_diag = {diag_emit, NULL, 0, 0};
     61 
     62 /* ---- failure tracking --------------------------------------------- */
     63 
     64 static int g_failures;
     65 static const char* g_test_name = "?";
     66 #define EXPECT(cond, ...)                                                    \
     67   do {                                                                       \
     68     if (!(cond)) {                                                           \
     69       fprintf(stderr, "FAIL [%s] %s:%d: ", g_test_name, __FILE__, __LINE__); \
     70       fprintf(stderr, __VA_ARGS__);                                          \
     71       fputc('\n', stderr);                                                   \
     72       g_failures++;                                                          \
     73     }                                                                        \
     74   } while (0)
     75 
     76 /* ---- target builders ---------------------------------------------- */
     77 
     78 static void target_x64_windows(KitTargetSpec* t) {
     79   memset(t, 0, sizeof *t);
     80   t->arch = KIT_ARCH_X86_64;
     81   t->os = KIT_OS_WINDOWS;
     82   t->obj = KIT_OBJ_COFF;
     83   t->ptr_size = 8;
     84   t->ptr_align = 8;
     85   t->big_endian = false;
     86   t->pic = KIT_PIC_PIE;
     87   t->code_model = KIT_CM_SMALL;
     88 }
     89 
     90 static void target_aa64_windows(KitTargetSpec* t) {
     91   memset(t, 0, sizeof *t);
     92   t->arch = KIT_ARCH_ARM_64;
     93   t->os = KIT_OS_WINDOWS;
     94   t->obj = KIT_OBJ_COFF;
     95   t->ptr_size = 8;
     96   t->ptr_align = 8;
     97   t->big_endian = false;
     98   t->pic = KIT_PIC_PIE;
     99   t->code_model = KIT_CM_SMALL;
    100 }
    101 
    102 /* ---- shape helpers ------------------------------------------------- */
    103 
    104 static int sym_eq_str(Pool* p, Sym s, const char* want) {
    105   Slice got = pool_slice(p, s);
    106   size_t wlen = strlen(want);
    107   return got.s && got.len == wlen && memcmp(got.s, want, got.len) == 0;
    108 }
    109 
    110 static const Section* find_section_named(const ObjBuilder* ob, Pool* p,
    111                                          const char* want) {
    112   u32 n = obj_section_count(ob);
    113   for (u32 i = 1; i < n; ++i) {
    114     const Section* s = obj_section_get(ob, i);
    115     if (s->removed) continue;
    116     if (sym_eq_str(p, s->name, want)) return s;
    117   }
    118   return NULL;
    119 }
    120 
    121 static ObjSecId find_section_id(const ObjBuilder* ob, Pool* p,
    122                                 const char* want) {
    123   u32 n = obj_section_count(ob);
    124   for (u32 i = 1; i < n; ++i) {
    125     const Section* s = obj_section_get(ob, i);
    126     if (s->removed) continue;
    127     if (sym_eq_str(p, s->name, want)) return i;
    128   }
    129   return OBJ_SEC_NONE;
    130 }
    131 
    132 static ObjSymId find_sym_named(const ObjBuilder* ob, Pool* p,
    133                                const char* want) {
    134   ObjSymIter* it = obj_symiter_new(ob);
    135   ObjSymEntry e;
    136   ObjSymId found = OBJ_SYM_NONE;
    137   while (obj_symiter_next(it, &e)) {
    138     if (e.sym->removed) continue;
    139     if (sym_eq_str(p, e.sym->name, want)) {
    140       found = e.id;
    141       break;
    142     }
    143   }
    144   obj_symiter_free(it);
    145   return found;
    146 }
    147 
    148 /* ---- emit / read driver ------------------------------------------- */
    149 
    150 /* Emit ob into a fresh malloc()ed buffer.  Caller frees *out_buf.
    151  * Returns 0 on success, non-zero on failure. */
    152 static int emit_to_buf(Compiler* c, ObjBuilder* ob, uint8_t** out_buf,
    153                        size_t* out_len) {
    154   KitWriter* w = NULL;
    155   if (kit_writer_mem(&g_heap, &w) != KIT_OK || !w) return -1;
    156   emit_coff(c, ob, w);
    157   size_t n = 0;
    158   const uint8_t* data = kit_writer_mem_bytes(w, &n);
    159   uint8_t* buf = (uint8_t*)malloc(n ? n : 1);
    160   if (!buf) {
    161     kit_writer_close(w);
    162     return -1;
    163   }
    164   if (n) memcpy(buf, data, n);
    165   kit_writer_close(w);
    166   *out_buf = buf;
    167   *out_len = n;
    168   return 0;
    169 }
    170 
    171 /* Debug helper: dump bytes side-by-side to stderr. */
    172 static void dump_diff(const uint8_t* a, const uint8_t* b, size_t n) {
    173   for (size_t i = 0; i < n; i += 16) {
    174     fprintf(stderr, "%04zx ", i);
    175     for (size_t j = 0; j < 16 && i + j < n; ++j) {
    176       fprintf(stderr, "%02x%c", a[i + j],
    177               (i + j < n && a[i + j] != b[i + j]) ? '*' : ' ');
    178     }
    179     fprintf(stderr, " | ");
    180     for (size_t j = 0; j < 16 && i + j < n; ++j) {
    181       fprintf(stderr, "%02x%c", b[i + j],
    182               (i + j < n && a[i + j] != b[i + j]) ? '*' : ' ');
    183     }
    184     fprintf(stderr, "\n");
    185   }
    186 }
    187 
    188 /* Three-stage round-trip workflow:
    189  *
    190  *   emit_coff(in)  -> b1
    191  *   read_coff(b1)  -> mid
    192  *   verify(mid)
    193  *   emit_coff(mid) -> b2
    194  *   EXPECT(b1 == b2)   [skipped when expect_byte_stable == 0]
    195  *
    196  * `verify_fn` runs against the readback ObjBuilder (mid). */
    197 static void run_roundtrip_ex(Compiler* c, ObjBuilder* in,
    198                              void (*verify_fn)(const ObjBuilder*, Pool*),
    199                              int expect_byte_stable) {
    200   uint8_t* b1 = NULL;
    201   size_t n1 = 0;
    202   if (emit_to_buf(c, in, &b1, &n1) != 0) {
    203     EXPECT(0, "emit_to_buf #1 failed");
    204     return;
    205   }
    206   /* Header sanity: little-endian machine + nsections must be present. */
    207   EXPECT(n1 >= 20, "emit_coff #1 produced %zu bytes (< 20)", n1);
    208 
    209   ObjBuilder* mid = read_coff(c, "roundtrip", b1, n1);
    210   EXPECT(mid != NULL, "read_coff returned NULL");
    211   if (!mid) {
    212     free(b1);
    213     return;
    214   }
    215 
    216   if (verify_fn) verify_fn(mid, c->global);
    217 
    218   uint8_t* b2 = NULL;
    219   size_t n2 = 0;
    220   if (emit_to_buf(c, mid, &b2, &n2) != 0) {
    221     EXPECT(0, "emit_to_buf #2 failed");
    222     obj_free(mid);
    223     free(b1);
    224     return;
    225   }
    226 
    227   if (expect_byte_stable) {
    228     EXPECT(n1 == n2, "byte-stable round-trip size mismatch: %zu vs %zu", n1,
    229            n2);
    230     if (n1 == n2) {
    231       int differs = memcmp(b1, b2, n1) != 0;
    232       EXPECT(!differs, "byte-stable round-trip differs (size %zu)", n1);
    233       if (differs && getenv("KIT_COFF_DUMP_DIFF")) {
    234         fprintf(stderr, "--- b1 | b2 ---\n");
    235         dump_diff(b1, b2, n1);
    236       }
    237     }
    238   }
    239 
    240   obj_free(mid);
    241   free(b1);
    242   free(b2);
    243 }
    244 
    245 static void run_roundtrip(Compiler* c, ObjBuilder* in,
    246                           void (*verify_fn)(const ObjBuilder*, Pool*)) {
    247   run_roundtrip_ex(c, in, verify_fn, /*expect_byte_stable=*/1);
    248 }
    249 
    250 /* ---- compiler lifecycle ----------------------------------------------
    251  * KitContext must outlive the Compiler — compiler_init stashes the
    252  * pointer.  Use a file-scope context so make_compiler doesn't leave
    253  * the compiler with a dangling ctx. */
    254 
    255 static KitContext g_ctx;
    256 
    257 static Compiler* make_compiler(const KitTargetSpec* t) {
    258   memset(&g_ctx, 0, sizeof g_ctx);
    259   g_ctx.heap = &g_heap;
    260   g_ctx.diag = &g_diag;
    261   g_ctx.now = -1;
    262   KitTargetOptions opts;
    263   memset(&opts, 0, sizeof opts);
    264   opts.spec = *t;
    265   KitTarget* target = NULL;
    266   if (kit_target_new(&g_ctx, &opts, &target) != KIT_OK || !target) return NULL;
    267   KitCompiler* cc = NULL;
    268   if (kit_compiler_new(target, &g_ctx, &cc) != KIT_OK || !cc) {
    269     kit_target_free(target);
    270     return NULL;
    271   }
    272   return (Compiler*)cc;
    273 }
    274 
    275 static void free_compiler(Compiler* c) {
    276   if (!c) return;
    277   const KitTarget* target = kit_compiler_target((KitCompiler*)c);
    278   kit_compiler_free((KitCompiler*)c);
    279   kit_target_free((KitTarget*)target);
    280 }
    281 
    282 /* ---- payload bytes ------------------------------------------------- */
    283 
    284 /* x64: mov eax, 42 ; ret. */
    285 static const uint8_t TEXT_X64[6] = {
    286     0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3,
    287 };
    288 
    289 /* aa64: mov w0, #42 ; ret. */
    290 static const uint8_t TEXT_AA64[8] = {
    291     0x40, 0x05, 0x80, 0x52, 0xc0, 0x03, 0x5f, 0xd6,
    292 };
    293 
    294 /* ---- per-test verifiers / builders -------------------------------- */
    295 
    296 /* test_header_minimal_x64 / _aa64: a single .text section, no
    297  * relocations, no user symbols.  Exercises the file/section-header
    298  * encoder + the section-symbol synthesis path. */
    299 
    300 static void verify_header_minimal(const ObjBuilder* ob, Pool* p) {
    301   const Section* text = find_section_named(ob, p, ".text");
    302   EXPECT(text != NULL, ".text not present");
    303   if (text) {
    304     EXPECT(text->kind == SEC_TEXT, ".text kind=%u", text->kind);
    305     EXPECT((text->flags & SF_EXEC) != 0, ".text missing SF_EXEC");
    306     EXPECT((text->flags & SF_ALLOC) != 0, ".text missing SF_ALLOC");
    307   }
    308 }
    309 
    310 static void test_header_minimal_x64(void) {
    311   g_test_name = "header_minimal_x64";
    312   KitTargetSpec t;
    313   target_x64_windows(&t);
    314   Compiler* c = make_compiler(&t);
    315   EXPECT(c != NULL, "compiler_new");
    316   if (!c) return;
    317 
    318   if (setjmp(c->panic)) {
    319     compiler_run_cleanups(c);
    320     free_compiler(c);
    321     EXPECT(0, "panic during test");
    322     return;
    323   }
    324   ObjBuilder* ob = obj_new(c);
    325   Pool* p = c->global;
    326   Sym text = pool_intern_slice(p, SLICE_LIT(".text"));
    327   ObjSecId sec = obj_section(ob, text, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    328   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
    329   obj_finalize(ob);
    330 
    331   run_roundtrip(c, ob, verify_header_minimal);
    332 
    333   obj_free(ob);
    334   free_compiler(c);
    335 }
    336 
    337 static void test_header_minimal_aa64(void) {
    338   g_test_name = "header_minimal_aa64";
    339   KitTargetSpec t;
    340   target_aa64_windows(&t);
    341   Compiler* c = make_compiler(&t);
    342   EXPECT(c != NULL, "compiler_new");
    343   if (!c) return;
    344 
    345   if (setjmp(c->panic)) {
    346     compiler_run_cleanups(c);
    347     free_compiler(c);
    348     EXPECT(0, "panic during test");
    349     return;
    350   }
    351   ObjBuilder* ob = obj_new(c);
    352   Pool* p = c->global;
    353   Sym text = pool_intern_slice(p, SLICE_LIT(".text"));
    354   ObjSecId sec = obj_section(ob, text, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
    355   obj_write(ob, sec, TEXT_AA64, sizeof TEXT_AA64);
    356   obj_finalize(ob);
    357 
    358   run_roundtrip(c, ob, verify_header_minimal);
    359 
    360   obj_free(ob);
    361   free_compiler(c);
    362 }
    363 
    364 /* test_text_only_x64: .text + one defined global function symbol. */
    365 
    366 static void verify_text_only(const ObjBuilder* ob, Pool* p) {
    367   const Section* text = find_section_named(ob, p, ".text");
    368   EXPECT(text != NULL, ".text not present");
    369   ObjSymId main = find_sym_named(ob, p, "main");
    370   EXPECT(main != OBJ_SYM_NONE, "missing 'main' symbol");
    371   if (main) {
    372     const ObjSym* s = obj_symbol_get(ob, main);
    373     EXPECT(s->bind == SB_GLOBAL, "main bind=%u", s->bind);
    374     EXPECT(s->kind == SK_FUNC, "main kind=%u", s->kind);
    375     EXPECT(s->section_id != OBJ_SEC_NONE, "main has no section");
    376   }
    377 }
    378 
    379 static void test_text_only_x64(void) {
    380   g_test_name = "text_only_x64";
    381   KitTargetSpec t;
    382   target_x64_windows(&t);
    383   Compiler* c = make_compiler(&t);
    384   if (!c) {
    385     EXPECT(0, "compiler_new");
    386     return;
    387   }
    388   if (setjmp(c->panic)) {
    389     compiler_run_cleanups(c);
    390     free_compiler(c);
    391     EXPECT(0, "panic");
    392     return;
    393   }
    394   ObjBuilder* ob = obj_new(c);
    395   Pool* p = c->global;
    396   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    397   Sym mn = pool_intern_slice(p, SLICE_LIT("main"));
    398   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    399   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
    400   obj_symbol(ob, mn, SB_GLOBAL, SK_FUNC, sec, 0, sizeof TEXT_X64);
    401   obj_finalize(ob);
    402 
    403   run_roundtrip(c, ob, verify_text_only);
    404 
    405   obj_free(ob);
    406   free_compiler(c);
    407 }
    408 
    409 static void test_text_only_aa64(void) {
    410   g_test_name = "text_only_aa64";
    411   KitTargetSpec t;
    412   target_aa64_windows(&t);
    413   Compiler* c = make_compiler(&t);
    414   if (!c) {
    415     EXPECT(0, "compiler_new");
    416     return;
    417   }
    418   if (setjmp(c->panic)) {
    419     compiler_run_cleanups(c);
    420     free_compiler(c);
    421     EXPECT(0, "panic");
    422     return;
    423   }
    424   ObjBuilder* ob = obj_new(c);
    425   Pool* p = c->global;
    426   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    427   Sym mn = pool_intern_slice(p, SLICE_LIT("main"));
    428   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
    429   obj_write(ob, sec, TEXT_AA64, sizeof TEXT_AA64);
    430   obj_symbol(ob, mn, SB_GLOBAL, SK_FUNC, sec, 0, sizeof TEXT_AA64);
    431   obj_finalize(ob);
    432 
    433   run_roundtrip(c, ob, verify_text_only);
    434 
    435   obj_free(ob);
    436   free_compiler(c);
    437 }
    438 
    439 /* test_rodata: .rdata read-only data + a defined object symbol. */
    440 
    441 static void verify_rodata(const ObjBuilder* ob, Pool* p) {
    442   const Section* rd = find_section_named(ob, p, ".rdata");
    443   EXPECT(rd != NULL, ".rdata not present");
    444   if (rd) {
    445     EXPECT(rd->kind == SEC_RODATA, ".rdata kind=%u (want %u)", rd->kind,
    446            SEC_RODATA);
    447     EXPECT((rd->flags & SF_ALLOC) != 0, ".rdata missing SF_ALLOC");
    448     EXPECT((rd->flags & SF_WRITE) == 0, ".rdata wrongly has SF_WRITE");
    449   }
    450   ObjSymId k = find_sym_named(ob, p, "kMsg");
    451   EXPECT(k != OBJ_SYM_NONE, "missing 'kMsg' symbol");
    452 }
    453 
    454 static void test_rodata(void) {
    455   g_test_name = "rodata";
    456   KitTargetSpec t;
    457   target_x64_windows(&t);
    458   Compiler* c = make_compiler(&t);
    459   if (!c) {
    460     EXPECT(0, "compiler_new");
    461     return;
    462   }
    463   if (setjmp(c->panic)) {
    464     compiler_run_cleanups(c);
    465     free_compiler(c);
    466     EXPECT(0, "panic");
    467     return;
    468   }
    469   ObjBuilder* ob = obj_new(c);
    470   Pool* p = c->global;
    471   Sym rdn = pool_intern_slice(p, SLICE_LIT(".rdata"));
    472   Sym kn = pool_intern_slice(p, SLICE_LIT("kMsg"));
    473   ObjSecId sec = obj_section(ob, rdn, SEC_RODATA, SF_ALLOC, 8);
    474   static const uint8_t MSG[12] = "hello world\0";
    475   obj_write(ob, sec, MSG, sizeof MSG);
    476   obj_symbol(ob, kn, SB_GLOBAL, SK_OBJ, sec, 0, sizeof MSG);
    477   obj_finalize(ob);
    478 
    479   run_roundtrip(c, ob, verify_rodata);
    480 
    481   obj_free(ob);
    482   free_compiler(c);
    483 }
    484 
    485 /* test_bss: .bss section (NOBITS), one defined symbol, size > 0. */
    486 
    487 static void verify_bss(const ObjBuilder* ob, Pool* p) {
    488   const Section* bss = find_section_named(ob, p, ".bss");
    489   EXPECT(bss != NULL, ".bss not present");
    490   if (bss) {
    491     EXPECT(bss->kind == SEC_BSS, ".bss kind=%u (want %u)", bss->kind, SEC_BSS);
    492     EXPECT(bss->bss_size >= 64, ".bss size=%u (want >= 64)", bss->bss_size);
    493   }
    494 }
    495 
    496 static void test_bss(void) {
    497   g_test_name = "bss";
    498   KitTargetSpec t;
    499   target_x64_windows(&t);
    500   Compiler* c = make_compiler(&t);
    501   if (!c) {
    502     EXPECT(0, "compiler_new");
    503     return;
    504   }
    505   if (setjmp(c->panic)) {
    506     compiler_run_cleanups(c);
    507     free_compiler(c);
    508     EXPECT(0, "panic");
    509     return;
    510   }
    511   ObjBuilder* ob = obj_new(c);
    512   Pool* p = c->global;
    513   Sym bn = pool_intern_slice(p, SLICE_LIT(".bss"));
    514   Sym vn = pool_intern_slice(p, SLICE_LIT("g_buf"));
    515   ObjSecId sec = obj_section_ex(ob, bn, SEC_BSS, SSEM_NOBITS,
    516                                 SF_ALLOC | SF_WRITE, 16, 0, 0, 0);
    517   obj_reserve_bss(ob, sec, 64, 16);
    518   obj_symbol(ob, vn, SB_GLOBAL, SK_OBJ, sec, 0, 64);
    519   obj_finalize(ob);
    520 
    521   run_roundtrip(c, ob, verify_bss);
    522 
    523   obj_free(ob);
    524   free_compiler(c);
    525 }
    526 
    527 /* test_data_with_reloc_abs64_x64: .data with an 8-byte slot
    528  * relocated R_ABS64 against an undefined external. */
    529 
    530 static void verify_data_abs64(const ObjBuilder* ob, Pool* p) {
    531   ObjSecId data_id = find_section_id(ob, p, ".data");
    532   EXPECT(data_id != OBJ_SEC_NONE, ".data id");
    533   ObjSymId foo = find_sym_named(ob, p, "foo_extern");
    534   EXPECT(foo != OBJ_SYM_NONE, "missing 'foo_extern'");
    535   if (foo) {
    536     const ObjSym* s = obj_symbol_get(ob, foo);
    537     EXPECT(s->section_id == OBJ_SEC_NONE, "foo_extern not undef");
    538   }
    539   if (data_id == OBJ_SEC_NONE) return;
    540   u32 nr = obj_reloc_count(ob, data_id);
    541   EXPECT(nr == 1, ".data reloc count=%u (want 1)", nr);
    542   u32 total = obj_reloc_total(ob);
    543   const Reloc* found = NULL;
    544   for (u32 i = 0; i < total; ++i) {
    545     const Reloc* r = obj_reloc_at(ob, i);
    546     if (r->removed) continue;
    547     if (r->section_id == data_id) {
    548       found = r;
    549       break;
    550     }
    551   }
    552   EXPECT(found != NULL, "no reloc on .data");
    553   if (found) {
    554     EXPECT(found->kind == R_ABS64, ".data reloc kind=%u (want %u)", found->kind,
    555            R_ABS64);
    556     EXPECT(found->offset == 0, ".data reloc offset=%u", found->offset);
    557   }
    558 }
    559 
    560 static void test_data_with_reloc_abs64_x64(void) {
    561   g_test_name = "data_with_reloc_abs64_x64";
    562   KitTargetSpec t;
    563   target_x64_windows(&t);
    564   Compiler* c = make_compiler(&t);
    565   if (!c) {
    566     EXPECT(0, "compiler_new");
    567     return;
    568   }
    569   if (setjmp(c->panic)) {
    570     compiler_run_cleanups(c);
    571     free_compiler(c);
    572     EXPECT(0, "panic");
    573     return;
    574   }
    575   ObjBuilder* ob = obj_new(c);
    576   Pool* p = c->global;
    577   Sym dn = pool_intern_slice(p, SLICE_LIT(".data"));
    578   Sym fn = pool_intern_slice(p, SLICE_LIT("foo_extern"));
    579   ObjSecId sec = obj_section(ob, dn, SEC_DATA, SF_ALLOC | SF_WRITE, 8);
    580   static const uint8_t zero8[8] = {0};
    581   obj_write(ob, sec, zero8, sizeof zero8);
    582   ObjSymId foo = obj_symbol(ob, fn, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    583   obj_reloc(ob, sec, 0, R_ABS64, foo, 0);
    584   obj_finalize(ob);
    585 
    586   run_roundtrip(c, ob, verify_data_abs64);
    587 
    588   obj_free(ob);
    589   free_compiler(c);
    590 }
    591 
    592 static void test_data_with_reloc_abs64_aa64(void) {
    593   g_test_name = "data_with_reloc_abs64_aa64";
    594   KitTargetSpec t;
    595   target_aa64_windows(&t);
    596   Compiler* c = make_compiler(&t);
    597   if (!c) {
    598     EXPECT(0, "compiler_new");
    599     return;
    600   }
    601   if (setjmp(c->panic)) {
    602     compiler_run_cleanups(c);
    603     free_compiler(c);
    604     EXPECT(0, "panic");
    605     return;
    606   }
    607   ObjBuilder* ob = obj_new(c);
    608   Pool* p = c->global;
    609   Sym dn = pool_intern_slice(p, SLICE_LIT(".data"));
    610   Sym fn = pool_intern_slice(p, SLICE_LIT("foo_extern"));
    611   ObjSecId sec = obj_section(ob, dn, SEC_DATA, SF_ALLOC | SF_WRITE, 8);
    612   static const uint8_t zero8[8] = {0};
    613   obj_write(ob, sec, zero8, sizeof zero8);
    614   ObjSymId foo = obj_symbol(ob, fn, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    615   obj_reloc(ob, sec, 0, R_ABS64, foo, 0);
    616   obj_finalize(ob);
    617 
    618   run_roundtrip(c, ob, verify_data_abs64);
    619 
    620   obj_free(ob);
    621   free_compiler(c);
    622 }
    623 
    624 /* test_data_with_reloc_rel32_x64: .text with a REL32 relocation
    625  * referencing an external symbol (call thunk). */
    626 
    627 static void verify_rel32(const ObjBuilder* ob, Pool* p) {
    628   ObjSecId text_id = find_section_id(ob, p, ".text");
    629   EXPECT(text_id != OBJ_SEC_NONE, ".text id");
    630   ObjSymId helper = find_sym_named(ob, p, "helper");
    631   EXPECT(helper != OBJ_SYM_NONE, "missing 'helper'");
    632   if (text_id == OBJ_SEC_NONE) return;
    633   u32 nr = obj_reloc_count(ob, text_id);
    634   EXPECT(nr == 1, ".text reloc count=%u (want 1)", nr);
    635   u32 total = obj_reloc_total(ob);
    636   for (u32 i = 0; i < total; ++i) {
    637     const Reloc* r = obj_reloc_at(ob, i);
    638     if (r->removed) continue;
    639     if (r->section_id != text_id) continue;
    640     EXPECT(r->kind == R_PC32, "reloc kind=%u (want R_PC32=%u)", r->kind,
    641            R_PC32);
    642   }
    643 }
    644 
    645 static void test_data_with_reloc_rel32_x64(void) {
    646   g_test_name = "reloc_rel32_x64";
    647   KitTargetSpec t;
    648   target_x64_windows(&t);
    649   Compiler* c = make_compiler(&t);
    650   if (!c) {
    651     EXPECT(0, "compiler_new");
    652     return;
    653   }
    654   if (setjmp(c->panic)) {
    655     compiler_run_cleanups(c);
    656     free_compiler(c);
    657     EXPECT(0, "panic");
    658     return;
    659   }
    660   ObjBuilder* ob = obj_new(c);
    661   Pool* p = c->global;
    662   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    663   Sym hn = pool_intern_slice(p, SLICE_LIT("helper"));
    664   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    665   /* call helper ; ret  — e8 disp32 c3 (disp filled by reloc). */
    666   static const uint8_t bytes[6] = {0xe8, 0, 0, 0, 0, 0xc3};
    667   obj_write(ob, sec, bytes, sizeof bytes);
    668   /* Undef symbol kind: SK_UNDEF — matches what real COFF inputs carry.
    669    * SK_FUNC + section_id == 0 emits Type=function but the reader collapses
    670    * to SK_UNDEF on readback (no "undef function" kind in kit's model),
    671    * which breaks byte stability.  See CORPUS.md §10. */
    672   ObjSymId helper = obj_symbol(ob, hn, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    673   obj_reloc(ob, sec, 1, R_PC32, helper, 0);
    674   obj_finalize(ob);
    675 
    676   run_roundtrip(c, ob, verify_rel32);
    677 
    678   obj_free(ob);
    679   free_compiler(c);
    680 }
    681 
    682 static void verify_rel32_inline_addend(const ObjBuilder* ob, Pool* p) {
    683   ObjSecId text_id = find_section_id(ob, p, ".text");
    684   EXPECT(text_id != OBJ_SEC_NONE, ".text id");
    685   u32 total = obj_reloc_total(ob);
    686   const Reloc* found = NULL;
    687   for (u32 i = 0; i < total; ++i) {
    688     const Reloc* r = obj_reloc_at(ob, i);
    689     if (r->removed) continue;
    690     if (r->section_id != text_id) continue;
    691     found = r;
    692     break;
    693   }
    694   EXPECT(found != NULL, "no reloc on .text");
    695   if (found) {
    696     EXPECT(found->kind == R_PC32, "reloc kind=%u (want R_PC32=%u)",
    697            found->kind, R_PC32);
    698     EXPECT(found->addend == 0x124, "reloc addend=%lld (want 0x124)",
    699            (long long)found->addend);
    700     EXPECT(found->has_explicit_addend == 0, "inline addend marked explicit");
    701   }
    702 }
    703 
    704 static void test_reloc_rel32_inline_addend_x64(void) {
    705   g_test_name = "reloc_rel32_inline_addend_x64";
    706   KitTargetSpec t;
    707   target_x64_windows(&t);
    708   Compiler* c = make_compiler(&t);
    709   if (!c) {
    710     EXPECT(0, "compiler_new");
    711     return;
    712   }
    713   if (setjmp(c->panic)) {
    714     compiler_run_cleanups(c);
    715     free_compiler(c);
    716     EXPECT(0, "panic");
    717     return;
    718   }
    719   ObjBuilder* ob = obj_new(c);
    720   Pool* p = c->global;
    721   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    722   Sym hn = pool_intern_slice(p, SLICE_LIT("helper"));
    723   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    724   /* call helper+0x128; ret. COFF stores this addend inline. */
    725   static const uint8_t bytes[6] = {0xe8, 0x28, 0x01, 0, 0, 0xc3};
    726   obj_write(ob, sec, bytes, sizeof bytes);
    727   ObjSymId helper = obj_symbol(ob, hn, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    728   obj_reloc_ex(ob, sec, 1, R_PC32, helper, 0, 0, 0);
    729   obj_finalize(ob);
    730 
    731   run_roundtrip(c, ob, verify_rel32_inline_addend);
    732 
    733   obj_free(ob);
    734   free_compiler(c);
    735 }
    736 
    737 /* test_aa64_branch26: .text with a BRANCH26 (R_AARCH64_CALL26)
    738  * relocation against an external. */
    739 
    740 static void verify_aa64_branch26(const ObjBuilder* ob, Pool* p) {
    741   ObjSecId text_id = find_section_id(ob, p, ".text");
    742   EXPECT(text_id != OBJ_SEC_NONE, ".text id");
    743   u32 total = obj_reloc_total(ob);
    744   int seen = 0;
    745   for (u32 i = 0; i < total; ++i) {
    746     const Reloc* r = obj_reloc_at(ob, i);
    747     if (r->removed) continue;
    748     if (r->section_id != text_id) continue;
    749     EXPECT(r->kind == R_AARCH64_CALL26, "branch26 reloc kind=%u (want %u)",
    750            r->kind, R_AARCH64_CALL26);
    751     ++seen;
    752   }
    753   EXPECT(seen == 1, "branch26 reloc count=%d (want 1)", seen);
    754 }
    755 
    756 static void test_aa64_branch26(void) {
    757   g_test_name = "aa64_branch26";
    758   KitTargetSpec t;
    759   target_aa64_windows(&t);
    760   Compiler* c = make_compiler(&t);
    761   if (!c) {
    762     EXPECT(0, "compiler_new");
    763     return;
    764   }
    765   if (setjmp(c->panic)) {
    766     compiler_run_cleanups(c);
    767     free_compiler(c);
    768     EXPECT(0, "panic");
    769     return;
    770   }
    771   ObjBuilder* ob = obj_new(c);
    772   Pool* p = c->global;
    773   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    774   Sym cn = pool_intern_slice(p, SLICE_LIT("callee"));
    775   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
    776   /* bl callee ; ret  — both 4 bytes; disp filled by reloc. */
    777   static const uint8_t bytes[8] = {0, 0, 0, 0x94, 0xc0, 0x03, 0x5f, 0xd6};
    778   obj_write(ob, sec, bytes, sizeof bytes);
    779   /* See reloc_rel32_x64 note on SK_UNDEF for undef symbols. */
    780   ObjSymId callee = obj_symbol(ob, cn, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
    781   obj_reloc(ob, sec, 0, R_AARCH64_CALL26, callee, 0);
    782   obj_finalize(ob);
    783 
    784   run_roundtrip(c, ob, verify_aa64_branch26);
    785 
    786   obj_free(ob);
    787   free_compiler(c);
    788 }
    789 
    790 /* test_aa64_pagebase_pageoffset: ADRP + ADD pair against a .rdata
    791  * symbol — exercises both PAGEBASE_REL21 and PAGEOFFSET_12A. */
    792 
    793 static void verify_aa64_adrp_add(const ObjBuilder* ob, Pool* p) {
    794   ObjSecId text_id = find_section_id(ob, p, ".text");
    795   EXPECT(text_id != OBJ_SEC_NONE, ".text id");
    796   u32 total = obj_reloc_total(ob);
    797   int n_page = 0, n_off = 0;
    798   for (u32 i = 0; i < total; ++i) {
    799     const Reloc* r = obj_reloc_at(ob, i);
    800     if (r->removed) continue;
    801     if (r->section_id != text_id) continue;
    802     if (r->kind == R_AARCH64_ADR_PREL_PG_HI21) ++n_page;
    803     if (r->kind == R_AARCH64_ADD_ABS_LO12_NC) ++n_off;
    804   }
    805   EXPECT(n_page == 1, "ADRP reloc count=%d (want 1)", n_page);
    806   EXPECT(n_off == 1, "ADD lo12 reloc count=%d (want 1)", n_off);
    807 }
    808 
    809 static void test_aa64_pagebase_pageoffset(void) {
    810   g_test_name = "aa64_pagebase_pageoffset";
    811   KitTargetSpec t;
    812   target_aa64_windows(&t);
    813   Compiler* c = make_compiler(&t);
    814   if (!c) {
    815     EXPECT(0, "compiler_new");
    816     return;
    817   }
    818   if (setjmp(c->panic)) {
    819     compiler_run_cleanups(c);
    820     free_compiler(c);
    821     EXPECT(0, "panic");
    822     return;
    823   }
    824   ObjBuilder* ob = obj_new(c);
    825   Pool* p = c->global;
    826   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    827   Sym rdn = pool_intern_slice(p, SLICE_LIT(".rdata"));
    828   Sym kn = pool_intern_slice(p, SLICE_LIT("kStr"));
    829   ObjSecId tsec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 4);
    830   ObjSecId rsec = obj_section(ob, rdn, SEC_RODATA, SF_ALLOC, 8);
    831   /* adrp x0, kStr ; add x0, x0, :lo12:kStr ; ret. */
    832   static const uint8_t txt[12] = {
    833       0x00, 0, 0, 0x90, 0, 0, 0, 0x91, 0xc0, 0x03, 0x5f, 0xd6,
    834   };
    835   obj_write(ob, tsec, txt, sizeof txt);
    836   static const uint8_t str[6] = "hello";
    837   obj_write(ob, rsec, str, sizeof str);
    838   ObjSymId kStr = obj_symbol(ob, kn, SB_LOCAL, SK_OBJ, rsec, 0, sizeof str);
    839   obj_reloc(ob, tsec, 0, R_AARCH64_ADR_PREL_PG_HI21, kStr, 0);
    840   obj_reloc(ob, tsec, 4, R_AARCH64_ADD_ABS_LO12_NC, kStr, 0);
    841   obj_finalize(ob);
    842 
    843   run_roundtrip(c, ob, verify_aa64_adrp_add);
    844 
    845   obj_free(ob);
    846   free_compiler(c);
    847 }
    848 
    849 /* test_long_section_name: section whose name exceeds 8 bytes,
    850  * triggering the "/N" strtab-spill encoding. */
    851 
    852 static void verify_long_section_name(const ObjBuilder* ob, Pool* p) {
    853   const Section* s = find_section_named(ob, p, ".text$long_name_section");
    854   EXPECT(s != NULL, "long-named section not present");
    855   if (s) EXPECT(s->kind == SEC_TEXT, "long section kind=%u", s->kind);
    856 }
    857 
    858 static void test_long_section_name(void) {
    859   g_test_name = "long_section_name";
    860   KitTargetSpec t;
    861   target_x64_windows(&t);
    862   Compiler* c = make_compiler(&t);
    863   if (!c) {
    864     EXPECT(0, "compiler_new");
    865     return;
    866   }
    867   if (setjmp(c->panic)) {
    868     compiler_run_cleanups(c);
    869     free_compiler(c);
    870     EXPECT(0, "panic");
    871     return;
    872   }
    873   ObjBuilder* ob = obj_new(c);
    874   Pool* p = c->global;
    875   Sym nm = pool_intern_slice(p, SLICE_LIT(".text$long_name_section"));
    876   ObjSecId sec = obj_section(ob, nm, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    877   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
    878   obj_finalize(ob);
    879 
    880   run_roundtrip(c, ob, verify_long_section_name);
    881 
    882   obj_free(ob);
    883   free_compiler(c);
    884 }
    885 
    886 /* test_long_symbol_name: symbol whose name exceeds 8 bytes — uses
    887  * the LongName (Zeroes=0, Offset) wire form. */
    888 
    889 static void verify_long_symbol_name(const ObjBuilder* ob, Pool* p) {
    890   ObjSymId s = find_sym_named(ob, p, "very_long_symbol_name");
    891   EXPECT(s != OBJ_SYM_NONE, "long-named symbol not present");
    892   if (s) {
    893     const ObjSym* sym = obj_symbol_get(ob, s);
    894     EXPECT(sym->bind == SB_GLOBAL, "long sym bind=%u", sym->bind);
    895     EXPECT(sym->kind == SK_FUNC, "long sym kind=%u", sym->kind);
    896   }
    897 }
    898 
    899 static void test_long_symbol_name(void) {
    900   g_test_name = "long_symbol_name";
    901   KitTargetSpec t;
    902   target_x64_windows(&t);
    903   Compiler* c = make_compiler(&t);
    904   if (!c) {
    905     EXPECT(0, "compiler_new");
    906     return;
    907   }
    908   if (setjmp(c->panic)) {
    909     compiler_run_cleanups(c);
    910     free_compiler(c);
    911     EXPECT(0, "panic");
    912     return;
    913   }
    914   ObjBuilder* ob = obj_new(c);
    915   Pool* p = c->global;
    916   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    917   Sym sn = pool_intern_slice(p, SLICE_LIT("very_long_symbol_name"));
    918   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    919   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
    920   obj_symbol(ob, sn, SB_GLOBAL, SK_FUNC, sec, 0, sizeof TEXT_X64);
    921   obj_finalize(ob);
    922 
    923   run_roundtrip(c, ob, verify_long_symbol_name);
    924 
    925   obj_free(ob);
    926   free_compiler(c);
    927 }
    928 
    929 /* test_weak_global: weak global symbol — IMAGE_SYM_CLASS_WEAK_EXTERNAL
    930  * with a weak-extern aux record. */
    931 
    932 static void verify_weak_global(const ObjBuilder* ob, Pool* p) {
    933   ObjSymId s = find_sym_named(ob, p, "weak_sym");
    934   EXPECT(s != OBJ_SYM_NONE, "weak_sym not present");
    935   if (s) {
    936     const ObjSym* sym = obj_symbol_get(ob, s);
    937     EXPECT(sym->bind == SB_WEAK, "weak_sym bind=%u (want SB_WEAK=%u)",
    938            sym->bind, SB_WEAK);
    939   }
    940 }
    941 
    942 static void test_weak_global(void) {
    943   g_test_name = "weak_global";
    944   KitTargetSpec t;
    945   target_x64_windows(&t);
    946   Compiler* c = make_compiler(&t);
    947   if (!c) {
    948     EXPECT(0, "compiler_new");
    949     return;
    950   }
    951   if (setjmp(c->panic)) {
    952     compiler_run_cleanups(c);
    953     free_compiler(c);
    954     EXPECT(0, "panic");
    955     return;
    956   }
    957   ObjBuilder* ob = obj_new(c);
    958   Pool* p = c->global;
    959   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
    960   Sym wn = pool_intern_slice(p, SLICE_LIT("weak_sym"));
    961   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
    962   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
    963   obj_symbol(ob, wn, SB_WEAK, SK_FUNC, sec, 0, sizeof TEXT_X64);
    964   obj_finalize(ob);
    965 
    966   run_roundtrip(c, ob, verify_weak_global);
    967 
    968   obj_free(ob);
    969   free_compiler(c);
    970 }
    971 
    972 /* test_common_symbol: COFF common — UNDEFINED section number with
    973  * Value > 0 holding the size. */
    974 
    975 static void verify_common_symbol(const ObjBuilder* ob, Pool* p) {
    976   ObjSymId s = find_sym_named(ob, p, "common_var");
    977   EXPECT(s != OBJ_SYM_NONE, "common_var not present");
    978   if (s) {
    979     const ObjSym* sym = obj_symbol_get(ob, s);
    980     EXPECT(sym->kind == SK_COMMON, "common_var kind=%u (want SK_COMMON=%u)",
    981            sym->kind, SK_COMMON);
    982     EXPECT(sym->size == 128, "common_var size=%llu (want 128)",
    983            (unsigned long long)sym->size);
    984   }
    985 }
    986 
    987 static void test_common_symbol(void) {
    988   g_test_name = "common_symbol";
    989   KitTargetSpec t;
    990   target_x64_windows(&t);
    991   Compiler* c = make_compiler(&t);
    992   if (!c) {
    993     EXPECT(0, "compiler_new");
    994     return;
    995   }
    996   if (setjmp(c->panic)) {
    997     compiler_run_cleanups(c);
    998     free_compiler(c);
    999     EXPECT(0, "panic");
   1000     return;
   1001   }
   1002   ObjBuilder* ob = obj_new(c);
   1003   Pool* p = c->global;
   1004   Sym cn = pool_intern_slice(p, SLICE_LIT("common_var"));
   1005   obj_symbol_ex(ob, cn, SB_GLOBAL, SV_DEFAULT, SK_COMMON, OBJ_SEC_NONE, 0, 128,
   1006                 1);
   1007   obj_finalize(ob);
   1008 
   1009   run_roundtrip(c, ob, verify_common_symbol);
   1010 
   1011   obj_free(ob);
   1012   free_compiler(c);
   1013 }
   1014 
   1015 /* test_comdat_group: two sections wired into one COMDAT group. */
   1016 
   1017 static void verify_comdat_group(const ObjBuilder* ob, Pool* p) {
   1018   const Section* tsec = find_section_named(ob, p, ".text$x");
   1019   const Section* dsec = find_section_named(ob, p, ".data$x");
   1020   EXPECT(tsec != NULL, ".text$x missing");
   1021   EXPECT(dsec != NULL, ".data$x missing");
   1022   if (tsec) EXPECT((tsec->flags & SF_GROUP) != 0, ".text$x missing SF_GROUP");
   1023   if (dsec) EXPECT((dsec->flags & SF_GROUP) != 0, ".data$x missing SF_GROUP");
   1024 
   1025   /* COFF encodes COMDAT per-section (each member section carries its
   1026    * own section-definition aux with the selection rule); the wire
   1027    * format has no SHT_GROUP-style "N-member" record.  read_coff
   1028    * therefore emits one ObjGroup per COMDAT section — two input
   1029    * COMDAT sections => two single-section groups after round-trip.
   1030    * Each carries the section's section-symbol as its signature. */
   1031   ObjGroupIter* it = obj_groupiter_new(ob);
   1032   ObjGroupEntry e;
   1033   int seen = 0;
   1034   u32 total_member_sections = 0;
   1035   while (obj_groupiter_next(it, &e)) {
   1036     if (e.group->removed) continue;
   1037     ++seen;
   1038     total_member_sections += e.group->nsections;
   1039   }
   1040   obj_groupiter_free(it);
   1041   EXPECT(seen == 2, "expected 2 groups after COMDAT round-trip, got %d", seen);
   1042   EXPECT(total_member_sections == 2, "total COMDAT member sections=%u (want 2)",
   1043          total_member_sections);
   1044 }
   1045 
   1046 static void test_comdat_group(void) {
   1047   g_test_name = "comdat_group";
   1048   KitTargetSpec t;
   1049   target_x64_windows(&t);
   1050   Compiler* c = make_compiler(&t);
   1051   if (!c) {
   1052     EXPECT(0, "compiler_new");
   1053     return;
   1054   }
   1055   if (setjmp(c->panic)) {
   1056     compiler_run_cleanups(c);
   1057     free_compiler(c);
   1058     EXPECT(0, "panic");
   1059     return;
   1060   }
   1061   ObjBuilder* ob = obj_new(c);
   1062   Pool* p = c->global;
   1063   /* Short section names (<= 8 bytes) — section names that overflow into
   1064    * the strtab don't round-trip COMDAT detection because the section
   1065    * symbol's name is truncated on emit but the reader compares the
   1066    * resolved long name.  See CORPUS.md §10 / src/obj/coff/read.c
   1067    * is_section_sym logic. */
   1068   Sym tn = pool_intern_slice(p, SLICE_LIT(".text$x"));
   1069   Sym dn = pool_intern_slice(p, SLICE_LIT(".data$x"));
   1070   Sym sign = pool_intern_slice(p, SLICE_LIT("inline_fn"));
   1071 
   1072   ObjSecId tsec =
   1073       obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC | SF_GROUP, 16);
   1074   ObjSecId dsec =
   1075       obj_section(ob, dn, SEC_DATA, SF_ALLOC | SF_WRITE | SF_GROUP, 8);
   1076   obj_write(ob, tsec, TEXT_X64, sizeof TEXT_X64);
   1077   static const uint8_t z8[8] = {0};
   1078   obj_write(ob, dsec, z8, sizeof z8);
   1079 
   1080   ObjSymId sig =
   1081       obj_symbol(ob, sign, SB_WEAK, SK_FUNC, tsec, 0, sizeof TEXT_X64);
   1082   ObjGroupId gid = obj_group(ob, sign, sig, KIT_OBJ_GROUP_COMDAT);
   1083   obj_group_add_section(ob, gid, tsec);
   1084   obj_group_add_section(ob, gid, dsec);
   1085   obj_section_set_group(ob, tsec, gid);
   1086   obj_section_set_group(ob, dsec, gid);
   1087 
   1088   obj_finalize(ob);
   1089 
   1090   run_roundtrip(c, ob, verify_comdat_group);
   1091 
   1092   obj_free(ob);
   1093   free_compiler(c);
   1094 }
   1095 
   1096 /* test_static_local_symbol: STATIC storage class — file-local symbol. */
   1097 
   1098 static void verify_static_local(const ObjBuilder* ob, Pool* p) {
   1099   ObjSymId s = find_sym_named(ob, p, "local_fn");
   1100   EXPECT(s != OBJ_SYM_NONE, "local_fn not present");
   1101   if (s) {
   1102     const ObjSym* sym = obj_symbol_get(ob, s);
   1103     EXPECT(sym->bind == SB_LOCAL, "local_fn bind=%u (want SB_LOCAL=%u)",
   1104            sym->bind, SB_LOCAL);
   1105   }
   1106 }
   1107 
   1108 static void test_static_local_symbol(void) {
   1109   g_test_name = "static_local_symbol";
   1110   KitTargetSpec t;
   1111   target_x64_windows(&t);
   1112   Compiler* c = make_compiler(&t);
   1113   if (!c) {
   1114     EXPECT(0, "compiler_new");
   1115     return;
   1116   }
   1117   if (setjmp(c->panic)) {
   1118     compiler_run_cleanups(c);
   1119     free_compiler(c);
   1120     EXPECT(0, "panic");
   1121     return;
   1122   }
   1123   ObjBuilder* ob = obj_new(c);
   1124   Pool* p = c->global;
   1125   Sym tn = pool_intern_slice(p, SLICE_LIT(".text"));
   1126   Sym ln = pool_intern_slice(p, SLICE_LIT("local_fn"));
   1127   ObjSecId sec = obj_section(ob, tn, SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
   1128   obj_write(ob, sec, TEXT_X64, sizeof TEXT_X64);
   1129   obj_symbol(ob, ln, SB_LOCAL, SK_FUNC, sec, 0, sizeof TEXT_X64);
   1130   obj_finalize(ob);
   1131 
   1132   run_roundtrip(c, ob, verify_static_local);
   1133 
   1134   obj_free(ob);
   1135   free_compiler(c);
   1136 }
   1137 
   1138 /* test_section_symbol_synthesis: input has no explicit SK_SECTION
   1139  * symbol; readback should contain one per kept section (from the
   1140  * emitter-synthesized SECTION primary + section-definition aux). */
   1141 
   1142 static void verify_section_symbol_synthesis(const ObjBuilder* ob, Pool* p) {
   1143   ObjSymIter* it = obj_symiter_new(ob);
   1144   ObjSymEntry e;
   1145   int n_section_syms = 0;
   1146   while (obj_symiter_next(it, &e)) {
   1147     if (e.sym->removed) continue;
   1148     if (e.sym->kind == SK_SECTION) ++n_section_syms;
   1149   }
   1150   obj_symiter_free(it);
   1151   EXPECT(n_section_syms >= 1,
   1152          "no SK_SECTION symbols after round-trip (expected at least one)");
   1153   /* Best-effort: text + data + bss + rdata = 4. */
   1154   EXPECT(n_section_syms == 4,
   1155          "section-symbol count=%d (want 4: text/data/bss/rdata)",
   1156          n_section_syms);
   1157   (void)p;
   1158 }
   1159 
   1160 static void test_section_symbol_synthesis(void) {
   1161   g_test_name = "section_symbol_synthesis";
   1162   KitTargetSpec t;
   1163   target_x64_windows(&t);
   1164   Compiler* c = make_compiler(&t);
   1165   if (!c) {
   1166     EXPECT(0, "compiler_new");
   1167     return;
   1168   }
   1169   if (setjmp(c->panic)) {
   1170     compiler_run_cleanups(c);
   1171     free_compiler(c);
   1172     EXPECT(0, "panic");
   1173     return;
   1174   }
   1175   ObjBuilder* ob = obj_new(c);
   1176   Pool* p = c->global;
   1177 
   1178   ObjSecId text = obj_section(ob, pool_intern_slice(p, SLICE_LIT(".text")),
   1179                               SEC_TEXT, SF_ALLOC | SF_EXEC, 16);
   1180   obj_write(ob, text, TEXT_X64, sizeof TEXT_X64);
   1181   ObjSecId data = obj_section(ob, pool_intern_slice(p, SLICE_LIT(".data")),
   1182                               SEC_DATA, SF_ALLOC | SF_WRITE, 8);
   1183   static const uint8_t z8[8] = {0};
   1184   obj_write(ob, data, z8, sizeof z8);
   1185   ObjSecId rdata = obj_section(ob, pool_intern_slice(p, SLICE_LIT(".rdata")),
   1186                                SEC_RODATA, SF_ALLOC, 8);
   1187   obj_write(ob, rdata, "hi\0", 3);
   1188   ObjSecId bss =
   1189       obj_section_ex(ob, pool_intern_slice(p, SLICE_LIT(".bss")), SEC_BSS,
   1190                      SSEM_NOBITS, SF_ALLOC | SF_WRITE, 8, 0, 0, 0);
   1191   obj_reserve_bss(ob, bss, 16, 8);
   1192 
   1193   obj_finalize(ob);
   1194 
   1195   run_roundtrip(c, ob, verify_section_symbol_synthesis);
   1196 
   1197   obj_free(ob);
   1198   free_compiler(c);
   1199 }
   1200 
   1201 /* test_tls_section: ".tls$" section gets SF_TLS on readback (name-
   1202  * based detection in read_coff). */
   1203 
   1204 static void verify_tls_section(const ObjBuilder* ob, Pool* p) {
   1205   const Section* s = find_section_named(ob, p, ".tls$");
   1206   EXPECT(s != NULL, ".tls$ not present");
   1207   if (s) {
   1208     EXPECT((s->flags & SF_TLS) != 0, ".tls$ missing SF_TLS (flags=0x%x)",
   1209            s->flags);
   1210   }
   1211 }
   1212 
   1213 static void test_tls_section(void) {
   1214   g_test_name = "tls_section";
   1215   KitTargetSpec t;
   1216   target_x64_windows(&t);
   1217   Compiler* c = make_compiler(&t);
   1218   if (!c) {
   1219     EXPECT(0, "compiler_new");
   1220     return;
   1221   }
   1222   if (setjmp(c->panic)) {
   1223     compiler_run_cleanups(c);
   1224     free_compiler(c);
   1225     EXPECT(0, "panic");
   1226     return;
   1227   }
   1228   ObjBuilder* ob = obj_new(c);
   1229   Pool* p = c->global;
   1230   Sym nm = pool_intern_slice(p, SLICE_LIT(".tls$"));
   1231   Sym vn = pool_intern_slice(p, SLICE_LIT("tls_var"));
   1232   ObjSecId sec = obj_section(ob, nm, SEC_DATA, SF_ALLOC | SF_WRITE | SF_TLS, 8);
   1233   static const uint8_t z8[8] = {0};
   1234   obj_write(ob, sec, z8, sizeof z8);
   1235   obj_symbol(ob, vn, SB_GLOBAL, SK_OBJ, sec, 0, sizeof z8);
   1236   obj_finalize(ob);
   1237 
   1238   run_roundtrip(c, ob, verify_tls_section);
   1239 
   1240   obj_free(ob);
   1241   free_compiler(c);
   1242 }
   1243 
   1244 /* test_align_nibble: section with a non-trivial alignment (4096)
   1245  * round-trips via the ALIGN_4096BYTES nibble. */
   1246 
   1247 static void verify_align_nibble(const ObjBuilder* ob, Pool* p) {
   1248   const Section* s = find_section_named(ob, p, ".rdata");
   1249   EXPECT(s != NULL, ".rdata not present");
   1250   if (s) {
   1251     EXPECT(s->align == 4096, ".rdata align=%u (want 4096)", s->align);
   1252   }
   1253 }
   1254 
   1255 static void test_align_nibble(void) {
   1256   g_test_name = "align_nibble";
   1257   KitTargetSpec t;
   1258   target_x64_windows(&t);
   1259   Compiler* c = make_compiler(&t);
   1260   if (!c) {
   1261     EXPECT(0, "compiler_new");
   1262     return;
   1263   }
   1264   if (setjmp(c->panic)) {
   1265     compiler_run_cleanups(c);
   1266     free_compiler(c);
   1267     EXPECT(0, "panic");
   1268     return;
   1269   }
   1270   ObjBuilder* ob = obj_new(c);
   1271   Pool* p = c->global;
   1272   Sym nm = pool_intern_slice(p, SLICE_LIT(".rdata"));
   1273   ObjSecId sec = obj_section(ob, nm, SEC_RODATA, SF_ALLOC, 4096);
   1274   static const uint8_t z[16] = {0};
   1275   obj_write(ob, sec, z, sizeof z);
   1276   obj_finalize(ob);
   1277 
   1278   run_roundtrip(c, ob, verify_align_nibble);
   1279 
   1280   obj_free(ob);
   1281   free_compiler(c);
   1282 }
   1283 
   1284 /* test_empty_obj: no sections, no symbols.  Smallest valid .obj. */
   1285 
   1286 static void verify_empty_obj(const ObjBuilder* ob, Pool* p) {
   1287   (void)p;
   1288   u32 n = obj_section_count(ob);
   1289   /* obj_section_count includes the id-0 placeholder. */
   1290   int real = 0;
   1291   for (u32 i = 1; i < n; ++i) {
   1292     const Section* s = obj_section_get(ob, i);
   1293     if (!s->removed) ++real;
   1294   }
   1295   EXPECT(real == 0, "empty obj has %d sections after round-trip", real);
   1296 }
   1297 
   1298 static void test_empty_obj(void) {
   1299   g_test_name = "empty_obj";
   1300   KitTargetSpec t;
   1301   target_x64_windows(&t);
   1302   Compiler* c = make_compiler(&t);
   1303   if (!c) {
   1304     EXPECT(0, "compiler_new");
   1305     return;
   1306   }
   1307   if (setjmp(c->panic)) {
   1308     compiler_run_cleanups(c);
   1309     free_compiler(c);
   1310     EXPECT(0, "panic");
   1311     return;
   1312   }
   1313   ObjBuilder* ob = obj_new(c);
   1314   obj_finalize(ob);
   1315 
   1316   run_roundtrip(c, ob, verify_empty_obj);
   1317 
   1318   obj_free(ob);
   1319   free_compiler(c);
   1320 }
   1321 
   1322 /* ---- short-import (Microsoft .lib member) smoke ------------------ */
   1323 
   1324 /* Hand-build a 45-byte short-import record:
   1325  *   header (20)  + "ExitProcess\0" (12) + "KERNEL32.dll\0" (13) = 45
   1326  *   SizeOfData    = 25 (the two NUL-terminated strings).
   1327  *   Machine       = AMD64 (0x8664).
   1328  *   TypeFlags     = (Type=CODE) | (NameType=NAME << 2) = 0 | (1<<2) = 4.
   1329  *
   1330  * Verifies that read_coff dispatches to the short-import path, the
   1331  * synthesized ObjBuilder has the imported symbol and its `__imp_*`
   1332  * alias defined at OBJ_SEC_NONE (DSO-shape), and the providing DLL
   1333  * name is recoverable via obj_get_coff_import_dll. */
   1334 static void test_short_import_amd64(void) {
   1335   g_test_name = "short_import_amd64";
   1336   KitTargetSpec t;
   1337   target_x64_windows(&t);
   1338   Compiler* c = make_compiler(&t);
   1339   if (!c) {
   1340     EXPECT(0, "compiler_new");
   1341     return;
   1342   }
   1343   if (setjmp(c->panic)) {
   1344     compiler_run_cleanups(c);
   1345     free_compiler(c);
   1346     EXPECT(0, "panic during test");
   1347     return;
   1348   }
   1349 
   1350   static const char kSym[] = "ExitProcess";  /* 11 chars + NUL = 12 */
   1351   static const char kDll[] = "KERNEL32.dll"; /* 12 chars + NUL = 13 */
   1352   const uint32_t kSymLen = (uint32_t)(sizeof kSym - 1);
   1353   const uint32_t kDllLen = (uint32_t)(sizeof kDll - 1);
   1354   const uint32_t kDataLen = sizeof kSym + sizeof kDll; /* 12 + 13 = 25 */
   1355   const size_t kTotal = 20 + kDataLen;                 /* 45 */
   1356   uint8_t buf[64];
   1357   EXPECT(kTotal <= sizeof buf, "buf too small");
   1358   memset(buf, 0, kTotal);
   1359   /* Header. */
   1360   buf[0] = 0x00;
   1361   buf[1] = 0x00; /* Sig1 = 0 */
   1362   buf[2] = 0xFF;
   1363   buf[3] = 0xFF; /* Sig2 = 0xFFFF */
   1364   buf[4] = 0x00;
   1365   buf[5] = 0x00; /* Version = 0 */
   1366   buf[6] = 0x64;
   1367   buf[7] = 0x86; /* Machine = AMD64 (0x8664) */
   1368   /* TimeDateStamp = 0 (bytes 8..11 already 0). */
   1369   buf[12] = (uint8_t)(kDataLen & 0xFF);
   1370   buf[13] = (uint8_t)((kDataLen >> 8) & 0xFF);
   1371   buf[14] = (uint8_t)((kDataLen >> 16) & 0xFF);
   1372   buf[15] = (uint8_t)((kDataLen >> 24) & 0xFF);
   1373   /* OrdinalOrHint = 0 (16..17). */
   1374   /* TypeFlags = Type=CODE(0) | NameType=NAME(1)<<2 = 0x0004. */
   1375   buf[18] = 0x04;
   1376   buf[19] = 0x00;
   1377   /* Body: symbol name NUL DLL name NUL. */
   1378   memcpy(buf + 20, kSym, sizeof kSym);
   1379   memcpy(buf + 20 + sizeof kSym, kDll, sizeof kDll);
   1380 
   1381   ObjBuilder* ob = read_coff(c, "short-import", buf, kTotal);
   1382   EXPECT(ob != NULL, "read_coff returned NULL on short-import");
   1383   if (!ob) {
   1384     free_compiler(c);
   1385     return;
   1386   }
   1387 
   1388   Pool* p = c->global;
   1389   ObjSymId sid = find_sym_named(ob, p, kSym);
   1390   EXPECT(sid != OBJ_SYM_NONE, "missing imported symbol");
   1391   if (sid) {
   1392     const ObjSym* s = obj_symbol_get(ob, sid);
   1393     EXPECT(s->bind == SB_GLOBAL, "imported sym bind=%u (want SB_GLOBAL)",
   1394            s->bind);
   1395     EXPECT(s->kind == SK_FUNC, "imported sym kind=%u (want SK_FUNC)", s->kind);
   1396     EXPECT(s->section_id == OBJ_SEC_NONE,
   1397            "imported sym section_id=%u (want OBJ_SEC_NONE)",
   1398            (unsigned)s->section_id);
   1399   }
   1400 
   1401   ObjSymId imp_id = find_sym_named(ob, p, "__imp_ExitProcess");
   1402   EXPECT(imp_id != OBJ_SYM_NONE, "missing __imp_<name> alias");
   1403   if (imp_id) {
   1404     const ObjSym* s = obj_symbol_get(ob, imp_id);
   1405     EXPECT(s->bind == SB_GLOBAL, "__imp_ bind=%u (want SB_GLOBAL)", s->bind);
   1406     EXPECT(s->section_id == OBJ_SEC_NONE,
   1407            "__imp_ section_id=%u (want OBJ_SEC_NONE)", (unsigned)s->section_id);
   1408   }
   1409 
   1410   Sym dll = 0;
   1411   int got = obj_get_coff_import_dll(ob, &dll);
   1412   EXPECT(got, "obj_get_coff_import_dll returned 0 (annotation missing)");
   1413   if (got) EXPECT(sym_eq_str(p, dll, kDll), "DLL name mismatch");
   1414 
   1415   (void)kSymLen;
   1416   (void)kDllLen;
   1417   obj_free(ob);
   1418   free_compiler(c);
   1419 }
   1420 
   1421 /* Format/target detection from raw header bytes. ARM64EC (Machine 0xA641)
   1422  * must be recognized as a COFF object and resolve to plain AArch64 — the
   1423  * encoding is identical to ARM64 and the linker/codegen treat it as such. */
   1424 static void test_detect_arm64ec(void) {
   1425   /* Minimal COFF header prefix: Machine word then NumberOfSections. 0xA641
   1426    * is little-endian {0x41, 0xA6}. */
   1427   static const uint8_t arm64ec[] = {0x41, 0xA6, 0x00, 0x00};
   1428   static const uint8_t arm64[] = {0x64, 0xAA, 0x00, 0x00};
   1429   KitTargetSpec t;
   1430   EXPECT(kit_detect_fmt(arm64ec, sizeof arm64ec) == KIT_BIN_COFF,
   1431          "ARM64EC machine not detected as COFF");
   1432   memset(&t, 0, sizeof t);
   1433   EXPECT(kit_detect_target(arm64ec, sizeof arm64ec, &t) == KIT_OK,
   1434          "kit_detect_target failed on ARM64EC");
   1435   EXPECT(t.arch == KIT_ARCH_ARM_64, "ARM64EC did not resolve to AArch64");
   1436   EXPECT(t.obj == KIT_OBJ_COFF, "ARM64EC obj fmt not COFF");
   1437   /* Sanity: plain ARM64 still resolves the same arch. */
   1438   memset(&t, 0, sizeof t);
   1439   EXPECT(kit_detect_target(arm64, sizeof arm64, &t) == KIT_OK,
   1440          "kit_detect_target failed on ARM64");
   1441   EXPECT(t.arch == KIT_ARCH_ARM_64, "ARM64 did not resolve to AArch64");
   1442 }
   1443 
   1444 /* ---- driver -------------------------------------------------------- */
   1445 
   1446 typedef void (*TestFn)(void);
   1447 
   1448 static const struct {
   1449   const char* name;
   1450   TestFn fn;
   1451 } TESTS[] = {
   1452     {"header_minimal_x64", test_header_minimal_x64},
   1453     {"header_minimal_aa64", test_header_minimal_aa64},
   1454     {"text_only_x64", test_text_only_x64},
   1455     {"text_only_aa64", test_text_only_aa64},
   1456     {"rodata", test_rodata},
   1457     {"bss", test_bss},
   1458     {"data_with_reloc_abs64_x64", test_data_with_reloc_abs64_x64},
   1459     {"data_with_reloc_abs64_aa64", test_data_with_reloc_abs64_aa64},
   1460     {"reloc_rel32_x64", test_data_with_reloc_rel32_x64},
   1461     {"reloc_rel32_inline_addend_x64", test_reloc_rel32_inline_addend_x64},
   1462     {"aa64_branch26", test_aa64_branch26},
   1463     {"aa64_pagebase_pageoffset", test_aa64_pagebase_pageoffset},
   1464     {"long_section_name", test_long_section_name},
   1465     {"long_symbol_name", test_long_symbol_name},
   1466     {"weak_global", test_weak_global},
   1467     {"common_symbol", test_common_symbol},
   1468     {"comdat_group", test_comdat_group},
   1469     {"static_local_symbol", test_static_local_symbol},
   1470     {"section_symbol_synthesis", test_section_symbol_synthesis},
   1471     {"tls_section", test_tls_section},
   1472     {"align_nibble", test_align_nibble},
   1473     {"empty_obj", test_empty_obj},
   1474     {"short_import_amd64", test_short_import_amd64},
   1475     {"detect_arm64ec", test_detect_arm64ec},
   1476 };
   1477 static const size_t NTESTS = sizeof TESTS / sizeof TESTS[0];
   1478 
   1479 int main(void) {
   1480   for (size_t i = 0; i < NTESTS; ++i) {
   1481     int before = g_failures;
   1482     TESTS[i].fn();
   1483     if (g_failures == before) {
   1484       fprintf(stderr, "  ok  %s\n", TESTS[i].name);
   1485     } else {
   1486       fprintf(stderr, "  FAIL %s\n", TESTS[i].name);
   1487     }
   1488   }
   1489   if (g_failures) {
   1490     fprintf(stderr, "FAILED %d assertion(s) across %zu tests\n", g_failures,
   1491             NTESTS);
   1492     return 1;
   1493   }
   1494   fprintf(stderr, "OK %zu tests\n", NTESTS);
   1495   return 0;
   1496 }