kit

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

emit.c (30742B)


      1 /* Mach-O MH_OBJECT writer.  Walks a finalized ObjBuilder and emits a
      2  * 64-bit little-endian relocatable object via the supplied Writer.
      3  *
      4  * Layout strategy (MH_OBJECT — everything in one anonymous segment):
      5  *   1. plan Mach-O sections (one per non-symtab/strtab/rela ObjSection),
      6  *      mapping kit section names to (segname, sectname) pairs;
      7  *   2. partition ObjSyms into local / extdef / undef and assign final
      8  *      indices for LC_DYSYMTAB;
      9  *   3. build per-section relocation tables via the per-arch translator
     10  *      (only aarch64 is wired today);
     11  *   4. assign file offsets sequentially: header, load commands, section
     12  *      bytes, relocation tables, symbol table, string table;
     13  *   5. write header → load commands → section bytes → relocs → symtab
     14  *      → strtab.
     15  *
     16  * 64-bit little-endian only.  Big-endian / 32-bit panics at entry.
     17  *
     18  * Round-trip invariant: read_macho of
     19  * this output must produce an ObjBuilder shape-equivalent to the input,
     20  * modulo (a) Mach-O's mandatory (segname, sectname) pairing and (b)
     21  * any synthesized N_SECT symbols.  The (segname,sectname) form chosen
     22  * here is the canonical post-roundtrip shape — read_macho stores the
     23  * comma-joined "__SEG,__sect" form in Section.name so a re-emit
     24  * produces the same bytes. */
     25 
     26 #include <string.h>
     27 
     28 #include "core/arena.h"
     29 #include "core/buf.h"
     30 #include "core/bytes.h"
     31 #include "core/heap.h"
     32 #include "core/pool.h"
     33 #include "core/slice.h"
     34 #include "core/util.h"
     35 #include "obj/format.h"
     36 #include "obj/macho/macho.h"
     37 
     38 /* ---- LE writer helpers (Writer-based) ----
     39  * Thin aliases onto the shared writer_u*_le helpers (core/bytes.h). */
     40 
     41 #define wr_u32 writer_u32_le
     42 #define wr_u64 writer_u64_le
     43 
     44 static void wr_name16(Writer* w, const char* s, u32 len) {
     45   /* Mach-O section/segment names are 16-byte zero-padded fields.  Names
     46    * longer than 16 are truncated; the on-disk format leaves no room for
     47    * a longer encoding. */
     48   u8 buf[16];
     49   u32 n = len > 16 ? 16 : len;
     50   memcpy(buf, s, n);
     51   if (n < 16) memset(buf + n, 0, 16 - n);
     52   kit_writer_write(w, buf, 16);
     53 }
     54 
     55 /* ---- (segname,sectname) derivation ---- */
     56 
     57 /* Split a kit section name into Mach-O (segname, sectname) pair.
     58  * If `name` contains a comma, it is treated as already in
     59  * "__SEG,__sect" form and split at the first comma.  Otherwise we
     60  * derive the pair from SecKind, ignoring `name` (the input was an
     61  * ELF-shaped name like ".text" or ".rodata"). */
     62 typedef struct MSegSect {
     63   char segname[16];
     64   char sectname[16];
     65   u32 seg_len;
     66   u32 sect_len;
     67 } MSegSect;
     68 
     69 static void copy_fixed16(char* dst, u32* len_out, const char* src,
     70                          u32 src_len) {
     71   u32 n = src_len > 16 ? 16 : src_len;
     72   memcpy(dst, src, n);
     73   if (n < 16) memset(dst + n, 0, 16 - n);
     74   *len_out = n;
     75 }
     76 
     77 static void name_to_seg_sect(const char* name, u32 nlen, u16 sec_kind,
     78                              MSegSect* out) {
     79   /* Comma-separated form: take prefix as segname, suffix as sectname. */
     80   for (u32 i = 0; i < nlen; ++i) {
     81     if (name[i] == ',') {
     82       copy_fixed16(out->segname, &out->seg_len, name, i);
     83       copy_fixed16(out->sectname, &out->sect_len, name + i + 1, nlen - i - 1);
     84       return;
     85     }
     86   }
     87 
     88   /* Not comma-separated.  Derive from SecKind; ignore `name`. */
     89   const char* seg;
     90   const char* sect;
     91   switch (sec_kind) {
     92     case SEC_TEXT:
     93       seg = "__TEXT";
     94       sect = "__text";
     95       break;
     96     case SEC_RODATA:
     97       seg = "__TEXT";
     98       sect = "__const";
     99       break;
    100     case SEC_DATA:
    101       seg = "__DATA";
    102       sect = "__data";
    103       break;
    104     case SEC_BSS:
    105       seg = "__DATA";
    106       sect = "__bss";
    107       break;
    108     case SEC_DEBUG: {
    109       /* ".debug_*" → "__DWARF,__debug_*" (truncated to Mach-O's 16-byte
    110        * sectname, matching Apple's spelling). Shared with the DWARF
    111        * reader so the names round-trip. Any non-".debug_*" SEC_DEBUG
    112        * name falls back to the leading-dot strip. */
    113       char ds[17];
    114       seg = "__DWARF";
    115       copy_fixed16(out->segname, &out->seg_len, seg,
    116                    (u32)slice_from_cstr(seg).len);
    117       if (obj_macho_debug_sectname(name, nlen, ds)) {
    118         copy_fixed16(out->sectname, &out->sect_len, ds,
    119                      (u32)slice_from_cstr(ds).len);
    120       } else {
    121         sect = (nlen && name[0] == '.') ? name + 1 : name;
    122         copy_fixed16(out->sectname, &out->sect_len, sect,
    123                      (u32)((nlen && name[0] == '.') ? nlen - 1 : nlen));
    124       }
    125       return;
    126     }
    127     default:
    128       seg = "__DATA";
    129       sect = "__data";
    130       break;
    131   }
    132   copy_fixed16(out->segname, &out->seg_len, seg, (u32)slice_from_cstr(seg).len);
    133   copy_fixed16(out->sectname, &out->sect_len, sect,
    134                (u32)slice_from_cstr(sect).len);
    135 }
    136 
    137 /* ---- per-section plan ---- */
    138 
    139 typedef struct MSec {
    140   MSegSect ns;
    141   u64 addr;    /* assigned vmaddr within the segment */
    142   u64 size;    /* bytes (or bss size) */
    143   u32 fileoff; /* 0 for zerofill */
    144   u32 align;   /* power-of-two; stored as log2 in section_64.align */
    145   u32 reloff;  /* 0 if no relocs */
    146   u32 nreloc;
    147   u32 flags; /* S_TYPE | S_ATTR_* */
    148   u32 entsize;
    149   u32 obj_sec; /* originating ObjSecId */
    150   int is_zerofill;
    151   const Buf* obj_bytes; /* NULL when zerofill */
    152   u8* patch_bytes;      /* writable flattened copy with addends baked into
    153                          * reloc fields (x86_64); NULL → write obj_bytes verbatim */
    154   u8* relocs;           /* arena-allocated; nreloc * 8 bytes */
    155 } MSec;
    156 
    157 static u32 log2_align(u32 a) {
    158   u32 r = 0;
    159   while ((1u << r) < a) ++r;
    160   return r;
    161 }
    162 
    163 /* Lazily flatten m's section bytes into a writable arena buffer so x86_64
    164  * addends can be baked into the relocated fields. Returns NULL for zerofill. */
    165 static u8* macho_sec_writable(Compiler* c, MSec* m) {
    166   if (m->patch_bytes || !m->obj_bytes) return m->patch_bytes;
    167   u32 sz = m->obj_bytes->total;
    168   u8* p = (u8*)arena_alloc(c->scratch, sz ? sz : 1, _Alignof(u64));
    169   if (sz) buf_flatten(m->obj_bytes, p);
    170   m->patch_bytes = p;
    171   return p;
    172 }
    173 
    174 /* Write a `length`-encoded little-endian field (length 0/1/2/3 → 1/2/4/8 B). */
    175 static void macho_write_field(u8* at, u32 length, i64 v) {
    176   switch (length) {
    177     case 0: *at = (u8)v; break;
    178     case 1: wr_u16_le(at, (u16)v); break;
    179     case 2: wr_u32_le(at, (u32)v); break;
    180     default: wr_u64_le(at, (u64)v); break;
    181   }
    182 }
    183 
    184 static u32 section_flags_for(u16 sec_kind, u16 sec_flags, const char* sectname,
    185                              u32 sect_len) {
    186   u32 f = 0;
    187   if (sec_kind == SEC_TEXT || (sec_flags & SF_EXEC)) {
    188     f |= S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS;
    189   }
    190   if (sec_flags & SF_TLS) {
    191     /* Mach-O distinguishes three TLV section types by sectname:
    192      *   __thread_data  → S_THREAD_LOCAL_REGULAR (initial data)
    193      *   __thread_bss   → S_THREAD_LOCAL_ZEROFILL (zero-init data)
    194      *   __thread_vars  → S_THREAD_LOCAL_VARIABLES (descriptor records)
    195      * dyld dispatches its TLV-bootstrap pass off the S_TYPE; the
    196      * S_ATTR_* bits don't carry TLV semantics so we just emit the type. */
    197     if (sect_len >= 13 && memcmp(sectname, "__thread_vars", 13) == 0)
    198       return S_THREAD_LOCAL_VARIABLES;
    199     if (sec_kind == SEC_BSS) return S_THREAD_LOCAL_ZEROFILL;
    200     return S_THREAD_LOCAL_REGULAR;
    201   }
    202   if (sec_kind == SEC_BSS ||
    203       (sect_len >= 5 && memcmp(sectname, "__bss", 5) == 0)) {
    204     f |= S_ZEROFILL;
    205   }
    206   if (sec_flags & SF_STRINGS) {
    207     f = (f & ~SECTION_TYPE) | S_CSTRING_LITERALS;
    208   }
    209   if (sec_flags & SF_RETAIN) {
    210     f |= S_ATTR_NO_DEAD_STRIP;
    211   }
    212   /* Default S_REGULAR (0) for all others. */
    213   return f;
    214 }
    215 
    216 /* ---- symbol partition ---- */
    217 
    218 typedef struct MSym {
    219   ObjSymId obj_id;
    220   u32 strx; /* offset in string table */
    221   u8 n_type;
    222   u8 n_sect;
    223   u16 n_desc;
    224   u64 n_value;
    225 } MSym;
    226 
    227 static int sym_is_undef(const ObjSym* s) {
    228   return s->section_id == OBJ_SEC_NONE && s->kind != SK_ABS &&
    229          s->kind != SK_COMMON;
    230 }
    231 
    232 static int sym_is_extdef(const ObjSym* s) {
    233   if (sym_is_undef(s)) return 0;
    234   return s->bind == SB_GLOBAL || s->bind == SB_WEAK;
    235 }
    236 
    237 /* ---- string table ----
    238  *
    239  * Mach-O strtab: leading zero byte at offset 0 represents the empty
    240  * string.  Entries are NUL-terminated; we don't dedupe (small symbol
    241  * counts in v1; matches the simplest llvm output).  The "_" prefix on
    242  * C symbols is added inline in the writer below. */
    243 
    244 void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) {
    245   Heap* h = (Heap*)c->ctx->heap;
    246 
    247   /* Tombstone sweep first — strip/objcopy mutations and the historical
    248    * UNDEF prune are both expressed via Section.removed / ObjSym.removed
    249    * post-sweep. See obj_sweep_dead. */
    250   obj_sweep_dead(ob);
    251 
    252   /* ---- target validation ---------------------------------------- */
    253   const ObjFormatImpl* fmt = obj_format_lookup(KIT_OBJ_MACHO);
    254   const ObjMachoArchOps* macho =
    255       fmt && fmt->macho_arch ? fmt->macho_arch(c->target.arch) : NULL;
    256   u32 cputype, cpusubtype;
    257   u32 (*reloc_to)(u32);
    258   u32 (*reloc_pcrel)(u32);
    259   u32 (*reloc_length)(u32);
    260   if (!macho || !macho->reloc_to || !macho->reloc_pcrel ||
    261       !macho->reloc_length) {
    262     compiler_panic(c, SRCLOC_NONE, "emit_macho: unsupported target arch %u",
    263                    (u32)c->target.arch);
    264   }
    265   cputype = macho->cputype;
    266   cpusubtype = macho->cpusubtype;
    267   reloc_to = macho->reloc_to;
    268   reloc_pcrel = macho->reloc_pcrel;
    269   reloc_length = macho->reloc_length;
    270   if (c->target.big_endian) {
    271     compiler_panic(c, SRCLOC_NONE, "emit_macho: big-endian not supported");
    272   }
    273   if (c->target.ptr_size != 8) {
    274     compiler_panic(c, SRCLOC_NONE, "emit_macho: ptr_size %u (expected 8)",
    275                    (u32)c->target.ptr_size);
    276   }
    277 
    278   /* ---- pass 1: plan Mach-O sections ----------------------------- */
    279   u32 nobjsec = obj_section_count(ob);
    280   MSec* secs = arena_zarray(c->scratch, MSec, nobjsec ? nobjsec : 1);
    281   u32* obj_to_msec = arena_zarray(c->scratch, u32, nobjsec ? nobjsec : 1);
    282   u32 nsecs = 0;
    283   int has_explicit_atoms = obj_atom_count(ob) > 1u;
    284 
    285   for (u32 i = 1; i < nobjsec; ++i) {
    286     const Section* s = obj_section_get(ob, i);
    287     if (s->removed) continue; /* see obj_sweep_dead */
    288     /* Skip ELF-style synthetic sections that read_elf would have
    289      * filtered: SYMTAB / STRTAB / RELA / GROUP have no Mach-O
    290      * representation as data sections. */
    291     if (s->sem == SSEM_SYMTAB || s->sem == SSEM_STRTAB || s->sem == SSEM_RELA ||
    292         s->sem == SSEM_REL || s->sem == SSEM_GROUP) {
    293       continue;
    294     }
    295     Slice nm_s = pool_slice(c->global, s->name);
    296     const char* nm = nm_s.s;
    297     size_t nlen = nm_s.len;
    298     MSec* m = &secs[nsecs];
    299     name_to_seg_sect(nm ? nm : "", (u32)nlen, s->kind, &m->ns);
    300     m->obj_sec = i;
    301     m->align = s->align ? s->align : 1;
    302     m->entsize = s->entsize;
    303     /* Mach-O reader stashes the raw section.flags (S_TYPE | S_ATTR_*)
    304      * in Section.ext_type when reading a Mach-O input.  Use it
    305      * verbatim so attribute bits like S_ATTR_NO_DEAD_STRIP /
    306      * S_ATTR_LIVE_SUPPORT round-trip.  Fall back to the kind-derived
    307      * default for sections originating from non-Mach-O readers (e.g.
    308      * kit codegen). */
    309     if (s->ext_kind == OBJ_EXT_MACHO && s->ext_type) {
    310       m->flags = s->ext_type;
    311     } else {
    312       m->flags =
    313           section_flags_for(s->kind, s->flags, m->ns.sectname, m->ns.sect_len);
    314     }
    315     if (s->sem == SSEM_NOBITS || s->kind == SEC_BSS) {
    316       m->is_zerofill = 1;
    317       m->size = s->bss_size;
    318       m->obj_bytes = NULL;
    319       /* Preserve S_THREAD_LOCAL_ZEROFILL when SF_TLS routed us there;
    320        * a regular BSS section gets the plain S_ZEROFILL type. */
    321       u32 stype = m->flags & SECTION_TYPE;
    322       if (stype != S_THREAD_LOCAL_ZEROFILL)
    323         m->flags = (m->flags & ~SECTION_TYPE) | S_ZEROFILL;
    324     } else {
    325       m->is_zerofill = 0;
    326       m->size = s->bytes.total;
    327       m->obj_bytes = &s->bytes;
    328     }
    329     obj_to_msec[i] = nsecs + 1; /* 1-based: matches Mach-O n_sect. */
    330     nsecs++;
    331   }
    332   if (nsecs > 255u) {
    333     compiler_panic(c, SRCLOC_NONE,
    334                    "emit_macho: too many physical sections for Mach-O "
    335                    "symbol n_sect ordinals (%u > 255); use atom splitting "
    336                    "instead of physical split sections",
    337                    nsecs);
    338   }
    339 
    340   /* ---- pass 2: assign vmaddrs (segment-relative) and per-section
    341    *              flat-layout addresses.  MH_OBJECT keeps everything in
    342    *              one segment with vmaddr=0; section addr fields are
    343    *              relative offsets within the segment.
    344    *
    345    * Two-pass to match the conventional Mach-O `MH_OBJECT` layout:
    346    * non-zerofill sections come first in vmaddr order, then zerofill
    347    * sections at the tail.  Apple `as` and clang `-c` both lay out
    348    * this way, and roundtripping must reproduce it so symbol n_values
    349    * (which are segment-relative addresses) compare equal. */
    350   u64 cur_addr = 0;
    351   for (u32 i = 0; i < nsecs; ++i) {
    352     MSec* m = &secs[i];
    353     if (m->is_zerofill) continue;
    354     cur_addr = ALIGN_UP(cur_addr, (u64)m->align);
    355     m->addr = cur_addr;
    356     cur_addr += m->size;
    357   }
    358   for (u32 i = 0; i < nsecs; ++i) {
    359     MSec* m = &secs[i];
    360     if (!m->is_zerofill) continue;
    361     cur_addr = ALIGN_UP(cur_addr, (u64)m->align);
    362     m->addr = cur_addr;
    363     cur_addr += m->size;
    364   }
    365   u64 segment_vmsize = cur_addr;
    366 
    367   /* ---- pass 3: partition symbols (locals, extdefs, undefs) ------ */
    368   u32 nobjsym = 0;
    369   {
    370     ObjSymIter* it = obj_symiter_new(ob);
    371     ObjSymEntry e;
    372     while (obj_symiter_next(it, &e)) ++nobjsym;
    373     obj_symiter_free(it);
    374   }
    375 
    376   MSym* msyms = arena_zarray(c->scratch, MSym, nobjsym + 1);
    377   u32 nmsyms = 0;
    378   u32* sym_obj_to_macho =
    379       arena_zarray(c->scratch, u32, nobjsym + 2); /* obj_id -> mach idx */
    380 
    381   /* Shared deduping string-table builder (obj.h). Mach-O previously appended
    382    * names without dedup; one shared policy across formats both unifies the code
    383    * and shrinks the table (repeated names share one copy) at O(n) cost. */
    384   ObjStrtab strtab;
    385   obj_strtab_init(&strtab, h, /*dedup=*/1);
    386   /* Mach-O strtab convention: the first byte is " " (space) or NUL —
    387    * llvm/Apple emit a single NUL.  We start with NUL for offset 0. */
    388   {
    389     u8 z = 0;
    390     obj_strtab_put_raw(&strtab, &z, 1);
    391   }
    392 
    393   /* Emit in three passes so n_type/sect ordering matches LC_DYSYMTAB
    394    * (locals, then extdefs, then undefs).  The pass index IS the on-disk
    395    * class, so the per-pass emit counts ARE the LC_DYSYMTAB partition
    396    * sizes — count them inline instead of re-walking the symbols. */
    397   u32 nlocals = 0, nextdefs = 0, nundefs = 0;
    398   for (int pass = 0; pass < 3; ++pass) {
    399     ObjSymIter* it = obj_symiter_new(ob);
    400     ObjSymEntry e;
    401     while (obj_symiter_next(it, &e)) {
    402       const ObjSym* s = e.sym;
    403       if (s->removed) continue; /* spurious-UNDEF prune + explicit removal */
    404       int undef = sym_is_undef(s);
    405       int extdef = sym_is_extdef(s);
    406       int local = !undef && !extdef;
    407       int want =
    408           (pass == 0 && local) || (pass == 1 && extdef) || (pass == 2 && undef);
    409       if (!want) continue;
    410       MSym* ms = &msyms[nmsyms];
    411       ms->obj_id = e.id;
    412 
    413       Slice nm_s = pool_slice(c->global, s->name);
    414       const char* nm = nm_s.s;
    415       size_t nlen = nm_s.len;
    416       /* Mach-O symbol names are stored on disk verbatim — including
    417        * the leading `_` Apple toolchains use for C-source-level
    418        * symbols ("_main" for `int main()`).  kit treats the prefix
    419        * as part of the on-disk name, not a transform applied at emit.
    420        * Name-canonicalization for API callers (kit_jit_lookup,
    421        * link_set_entry) lives one layer up at the linker boundary
    422        * (link.c), so emit/read stay byte-for-byte stable. */
    423       ms->strx = (nlen && nm) ? obj_strtab_add(&strtab, nm, (u32)nlen) : 0;
    424 
    425       u8 type = 0;
    426       if (extdef) type |= N_EXT;
    427       if (s->vis == SV_HIDDEN || s->vis == SV_INTERNAL) {
    428         /* Mach-O encodes hidden externals as N_PEXT|N_EXT. */
    429         type |= N_PEXT;
    430       }
    431       u8 n_sect = NO_SECT;
    432       u16 n_desc = 0;
    433       u64 value = s->value;
    434 
    435       if (undef) {
    436         type |= N_UNDF;
    437         /* Undefined symbols with non-LOCAL bind are external references
    438          * (the common case — every `extern int x;`).  Setting N_EXT
    439          * matches what clang emits and what Apple `ld` expects. */
    440         if (s->bind == SB_GLOBAL || s->bind == SB_WEAK) type |= N_EXT;
    441         if (s->bind == SB_WEAK) n_desc |= N_WEAK_REF;
    442         value = 0;
    443       } else if (s->kind == SK_ABS) {
    444         type |= N_ABS;
    445       } else if (s->kind == SK_COMMON) {
    446         /* Mach-O common symbols are N_UNDF|N_EXT with n_value=size and
    447          * n_desc carrying log2(align) in the GET_COMM_ALIGN bits. */
    448         type = N_UNDF | N_EXT;
    449         value = s->size;
    450         u32 a = s->common_align ? (u32)s->common_align : 1;
    451         n_desc = (u16)(log2_align(a) << 8); /* GET_COMM_ALIGN field */
    452       } else {
    453         type |= N_SECT;
    454         u32 ms_idx = (s->section_id < nobjsec) ? obj_to_msec[s->section_id] : 0;
    455         if (ms_idx > 255u) {
    456           compiler_panic(c, SRCLOC_NONE,
    457                          "emit_macho: symbol section ordinal %u exceeds "
    458                          "Mach-O n_sect range",
    459                          ms_idx);
    460         }
    461         n_sect = (u8)ms_idx;
    462         if (n_sect && ms_idx <= nsecs) {
    463           value = secs[ms_idx - 1].addr + s->value;
    464         }
    465         if (s->bind == SB_WEAK) n_desc |= N_WEAK_DEF;
    466       }
    467 
    468       /* OR in any pass-through n_desc bits the reader stashed in
    469        * sym->flags (N_NO_DEAD_STRIP, etc.). The bits we already
    470        * compute (N_WEAK_DEF / N_WEAK_REF and the common-alignment
    471        * field) are already excluded by read_macho before stashing,
    472        * so a plain OR can't double-count. */
    473       n_desc |= s->flags;
    474       if (s->atom_subordinate && (type & N_TYPE) == N_SECT)
    475         n_desc |= N_ALT_ENTRY;
    476       {
    477         ObjAtomId aid = obj_atom_find_symbol(ob, e.id);
    478         const ObjAtom* atom = obj_atom_get(ob, aid);
    479         if (atom && atom->signature == e.id && (atom->flags & OBJ_ATOM_RETAIN))
    480           n_desc |= N_NO_DEAD_STRIP;
    481       }
    482 
    483       ms->n_type = type;
    484       ms->n_sect = n_sect;
    485       ms->n_desc = n_desc;
    486       ms->n_value = value;
    487 
    488       sym_obj_to_macho[e.id] = nmsyms + 1; /* 1-based index, 0 = none. */
    489       nmsyms++;
    490       if (pass == 0)
    491         ++nlocals;
    492       else if (pass == 1)
    493         ++nextdefs;
    494       else
    495         ++nundefs;
    496     }
    497     obj_symiter_free(it);
    498   }
    499 
    500   /* ---- pass 4: build per-section relocation tables -------------- */
    501   u32 total_relocs = obj_reloc_total(ob);
    502   for (u32 i = 0; i < nsecs; ++i) {
    503     MSec* m = &secs[i];
    504     u32 nr;
    505     const u32* rix = obj_reloc_section(ob, m->obj_sec, &nr);
    506     if (!nr) continue;
    507     /* Worst case: each reloc may be preceded by an ARM64_RELOC_ADDEND
    508      * pair entry.  We size the buffer for that upper bound. */
    509     u8* buf = (u8*)arena_alloc(c->scratch, (size_t)MACHO_RELOC_SIZE * nr * 2,
    510                                _Alignof(u32));
    511     u32 j = 0;
    512     /* rix[] lists this section's live relocs in ascending global order, so
    513      * each is in-section and non-removed by construction. The symdiff
    514      * pairing below still peeks the *global* next (ri+1) exactly as before. */
    515     for (u32 k = 0; k < nr; ++k) {
    516       u32 ri = rix[k];
    517       const Reloc* r = obj_reloc_at(ob, ri);
    518       if ((r->kind == R_ADD8 || r->kind == R_ADD16 || r->kind == R_ADD32 ||
    519            r->kind == R_ADD64) &&
    520           ri + 1u < total_relocs) {
    521         const Reloc* sub = obj_reloc_at(ob, ri + 1u);
    522         int paired = sub && sub->section_id == r->section_id &&
    523                      sub->offset == r->offset &&
    524                      ((r->kind == R_ADD8 && sub->kind == R_SUB8) ||
    525                       (r->kind == R_ADD16 && sub->kind == R_SUB16) ||
    526                       (r->kind == R_ADD32 && sub->kind == R_SUB32) ||
    527                       (r->kind == R_ADD64 && sub->kind == R_SUB64));
    528         if (paired) {
    529           u32 length = (r->kind == R_ADD64)   ? 3u
    530                        : (r->kind == R_ADD32) ? 2u
    531                        : (r->kind == R_ADD16) ? 1u
    532                                               : 0u;
    533           u32 add_idx;
    534           u32 sub_idx;
    535           u32 sub_type = c->target.arch == KIT_ARCH_ARM_64
    536                              ? ARM64_RELOC_SUBTRACTOR
    537                              : X86_64_RELOC_SUBTRACTOR;
    538           u32 unsigned_type = c->target.arch == KIT_ARCH_ARM_64
    539                                   ? ARM64_RELOC_UNSIGNED
    540                                   : X86_64_RELOC_UNSIGNED;
    541           if (r->sym == OBJ_SYM_NONE || sub->sym == OBJ_SYM_NONE) {
    542             compiler_panic(c, SRCLOC_NONE,
    543                            "emit_macho: symdiff reloc without symbol");
    544           }
    545           add_idx = sym_obj_to_macho[r->sym];
    546           sub_idx = sym_obj_to_macho[sub->sym];
    547           if (add_idx == 0 || sub_idx == 0) {
    548             compiler_panic(c, SRCLOC_NONE,
    549                            "emit_macho: symdiff reloc target not in symtab");
    550           }
    551           {
    552             u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
    553             wr_u32_le(slot + 0, (u32)r->offset);
    554             wr_u32_le(slot + 4, ((sub_idx - 1u) & 0x00ffffffu) |
    555                                     (length << 25) | (1u << 27) |
    556                                     ((sub_type & 0xfu) << 28));
    557             ++j;
    558           }
    559           {
    560             u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
    561             wr_u32_le(slot + 0, (u32)r->offset);
    562             wr_u32_le(slot + 4, ((add_idx - 1u) & 0x00ffffffu) |
    563                                     (length << 25) | (1u << 27) |
    564                                     ((unsigned_type & 0xfu) << 28));
    565             ++j;
    566           }
    567           /* Skip the SUB partner (global ri+1) — it is this section's next
    568            * live reloc iff it is globally adjacent in the bucket. */
    569           if (k + 1u < nr && rix[k + 1u] == ri + 1u) ++k;
    570           continue;
    571         }
    572       }
    573       u32 mtype = reloc_to(r->kind);
    574       if (mtype == (u32)-1) {
    575         compiler_panic(c, SRCLOC_NONE,
    576                        "emit_macho: unsupported reloc kind %u for arch %u",
    577                        (u32)r->kind, (u32)c->target.arch);
    578       }
    579       u32 pcrel = reloc_pcrel(r->kind);
    580       u32 length = reloc_length(r->kind);
    581 
    582       /* Resolve target — extern always 1 in our model (every Reloc has
    583        * an ObjSymId).  Skip relocs without a symbol — they would map to
    584        * a section-relative reloc which the v1 cgtarget never emits. */
    585       if (r->sym == OBJ_SYM_NONE) {
    586         compiler_panic(c, SRCLOC_NONE,
    587                        "emit_macho: reloc without symbol not supported "
    588                        "(sec=%u offset=%u kind=%u)",
    589                        (u32)r->section_id, (u32)r->offset, (u32)r->kind);
    590       }
    591       u32 mach_sym_idx = sym_obj_to_macho[r->sym];
    592       if (mach_sym_idx == 0) {
    593         compiler_panic(c, SRCLOC_NONE,
    594                        "emit_macho: reloc target sym %u not in symtab",
    595                        (u32)r->sym);
    596       }
    597       u32 r_symbolnum = mach_sym_idx - 1; /* Mach-O uses 0-based. */
    598 
    599       /* Non-zero addend (UNSIGNED carries its addend inline on both arches,
    600        * so its enum value 0 — shared by ARM64_RELOC_UNSIGNED and
    601        * X86_64_RELOC_UNSIGNED — is excluded here). The two arches diverge:
    602        *   AArch64 has no inline addend field for the PC-relative/GOT kinds,
    603        *     so a leading ARM64_RELOC_ADDEND pseudo-reloc carries it.
    604        *   x86_64 has no X86_64_RELOC_ADDEND; the relocated field holds the
    605        *     symbol-relative addend inline and the linker applies the PC bias.
    606        *     kit's addend is ELF-style (symrel − field_width for these
    607        *     PC-relative kinds), so bake symrel = addend + width back in. */
    608       if (r->addend != 0 && mtype != ARM64_RELOC_UNSIGNED) {
    609         if (c->target.arch == KIT_ARCH_ARM_64) {
    610           u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
    611           wr_u32_le(slot + 0, (u32)r->offset);
    612           /* ARM64_RELOC_ADDEND stores a signed 24-bit immediate in
    613            * r_symbolnum.  It is not a symbol-table reference; setting
    614            * r_extern would make readers interpret the addend as a symbol
    615            * index. */
    616           u32 packed = ((u32)(i64)r->addend & 0x00ffffffu) | (0u << 24) |
    617                        (length << 25) | (ARM64_RELOC_ADDEND << 28);
    618           wr_u32_le(slot + 4, packed);
    619           ++j;
    620         } else {
    621           u8* data = macho_sec_writable(c, m);
    622           if (data) {
    623             i64 symrel = r->addend + (pcrel ? (i64)(1u << length) : 0);
    624             macho_write_field(data + r->offset, length, symrel);
    625           }
    626         }
    627       }
    628 
    629       u8* slot = buf + (size_t)j * MACHO_RELOC_SIZE;
    630       wr_u32_le(slot + 0, (u32)r->offset);
    631       u32 packed = (r_symbolnum & 0x00ffffffu) | ((pcrel & 1u) << 24) |
    632                    ((length & 3u) << 25) | (1u << 27) /*extern*/ |
    633                    ((mtype & 0xfu) << 28);
    634       wr_u32_le(slot + 4, packed);
    635       ++j;
    636     }
    637     m->relocs = buf;
    638     m->nreloc = j;
    639   }
    640 
    641   /* ---- pass 5: assign file offsets ------------------------------ */
    642   /* Layout after the load-command block:
    643    *   section bytes (in order, respecting align)
    644    *   relocation tables (per section, 4-aligned)
    645    *   symbol table (8-aligned)
    646    *   string table */
    647   u32 nload_cmds =
    648       4; /* LC_SEGMENT_64 + LC_BUILD_VERSION + LC_SYMTAB + LC_DYSYMTAB */
    649   u32 segcmd_size = MACHO_SEGCMD64_SIZE + nsecs * MACHO_SECT64_SIZE;
    650   u32 build_version_size =
    651       24; /* fixed: cmd+cmdsize+platform+minos+sdk+ntools(0) */
    652   u32 sizeofcmds = segcmd_size + build_version_size + MACHO_SYMTAB_CMD_SIZE +
    653                    MACHO_DYSYMTAB_CMD_SIZE;
    654 
    655   u64 cur = MACHO_HDR64_SIZE + sizeofcmds;
    656   u32 fileoff_first = (u32)cur;
    657   for (u32 i = 0; i < nsecs; ++i) {
    658     MSec* m = &secs[i];
    659     if (m->is_zerofill) {
    660       m->fileoff = 0;
    661       continue;
    662     }
    663     cur = ALIGN_UP(cur, (u64)m->align);
    664     m->fileoff = (u32)cur;
    665     cur += m->size;
    666   }
    667 
    668   /* Reloc tables. */
    669   for (u32 i = 0; i < nsecs; ++i) {
    670     MSec* m = &secs[i];
    671     if (!m->nreloc) {
    672       m->reloff = 0;
    673       continue;
    674     }
    675     cur = ALIGN_UP(cur, (u64)4);
    676     m->reloff = (u32)cur;
    677     cur += (u64)m->nreloc * MACHO_RELOC_SIZE;
    678   }
    679 
    680   cur = ALIGN_UP(cur, (u64)8);
    681   u64 symoff = cur;
    682   cur += (u64)nmsyms * MACHO_NLIST64_SIZE;
    683   u64 stroff = cur;
    684   u32 strtab_size = obj_strtab_size(&strtab);
    685   cur += strtab_size;
    686 
    687   /* ---- pass 6: write the file ------------------------------------ */
    688   kit_writer_seek(w, 0);
    689 
    690   /* mach_header_64 */
    691   wr_u32(w, MH_MAGIC_64);
    692   wr_u32(w, cputype);
    693   wr_u32(w, cpusubtype);
    694   wr_u32(w, MH_OBJECT);
    695   wr_u32(w, nload_cmds);
    696   wr_u32(w, sizeofcmds);
    697   wr_u32(w, has_explicit_atoms ? MH_SUBSECTIONS_VIA_SYMBOLS : 0);
    698   wr_u32(w, 0); /* reserved */
    699 
    700   /* LC_SEGMENT_64 (anonymous, contains everything) */
    701   wr_u32(w, LC_SEGMENT_64);
    702   wr_u32(w, segcmd_size);
    703   wr_name16(w, "", 0);       /* segname: empty for MH_OBJECT */
    704   wr_u64(w, 0);              /* vmaddr */
    705   wr_u64(w, segment_vmsize); /* vmsize */
    706   wr_u64(w, fileoff_first);  /* fileoff */
    707   /* filesize = bytes covered by non-zerofill sections (post-section
    708    * file offset minus the start). */
    709   u64 filesize = 0;
    710   for (u32 i = 0; i < nsecs; ++i) {
    711     MSec* m = &secs[i];
    712     if (m->is_zerofill) continue;
    713     u64 end = (u64)m->fileoff + m->size;
    714     u64 begin = m->fileoff;
    715     if (end > filesize + fileoff_first) filesize = end - fileoff_first;
    716     (void)begin;
    717   }
    718   wr_u64(w, filesize);
    719   /* maxprot/initprot — VM_PROT_READ|WRITE|EXECUTE = 7 for object segs. */
    720   wr_u32(w, 7);
    721   wr_u32(w, 7);
    722   wr_u32(w, nsecs);
    723   wr_u32(w, 0); /* flags */
    724 
    725   /* sections inline within the segment command */
    726   for (u32 i = 0; i < nsecs; ++i) {
    727     MSec* m = &secs[i];
    728     wr_name16(w, m->ns.sectname, m->ns.sect_len);
    729     wr_name16(w, m->ns.segname, m->ns.seg_len);
    730     wr_u64(w, m->addr);
    731     wr_u64(w, m->size);
    732     wr_u32(w, m->fileoff);
    733     wr_u32(w, log2_align(m->align));
    734     wr_u32(w, m->reloff);
    735     wr_u32(w, m->nreloc);
    736     wr_u32(w, m->flags);
    737     wr_u32(w, 0);          /* reserved1 */
    738     wr_u32(w, m->entsize); /* reserved2 */
    739     wr_u32(w, 0);          /* reserved3 */
    740   }
    741 
    742   /* LC_BUILD_VERSION — platform follows the target OS, minos/sdk=14.0.0,
    743    * ntools=0.  The exact min-version isn't load-bearing for MH_OBJECT,
    744    * but Apple's `ld` warns when it's missing. */
    745   wr_u32(w, LC_BUILD_VERSION);
    746   wr_u32(w, build_version_size);
    747   wr_u32(w, macho_platform_for_target(c->target));
    748   wr_u32(w, (14u << 16) | 0); /* minos: 14.0.0 */
    749   wr_u32(w, (14u << 16) | 0); /* sdk:   14.0.0 */
    750   wr_u32(w, 0);               /* ntools */
    751 
    752   /* LC_SYMTAB */
    753   wr_u32(w, LC_SYMTAB);
    754   wr_u32(w, MACHO_SYMTAB_CMD_SIZE);
    755   wr_u32(w, (u32)symoff);
    756   wr_u32(w, nmsyms);
    757   wr_u32(w, (u32)stroff);
    758   wr_u32(w, strtab_size);
    759 
    760   /* LC_DYSYMTAB */
    761   wr_u32(w, LC_DYSYMTAB);
    762   wr_u32(w, MACHO_DYSYMTAB_CMD_SIZE);
    763   wr_u32(w, 0); /* ilocalsym */
    764   wr_u32(w, nlocals);
    765   wr_u32(w, nlocals);
    766   wr_u32(w, nextdefs);
    767   wr_u32(w, nlocals + nextdefs);
    768   wr_u32(w, nundefs);
    769   wr_u32(w, 0);
    770   wr_u32(w, 0); /* tocoff, ntoc */
    771   wr_u32(w, 0);
    772   wr_u32(w, 0); /* modtaboff, nmodtab */
    773   wr_u32(w, 0);
    774   wr_u32(w, 0); /* extrefsymoff, nextrefsyms */
    775   wr_u32(w, 0);
    776   wr_u32(w, 0); /* indirectsymoff, nindirectsyms */
    777   wr_u32(w, 0);
    778   wr_u32(w, 0); /* extreloff, nextrel */
    779   wr_u32(w, 0);
    780   wr_u32(w, 0); /* locreloff, nlocrel */
    781 
    782   /* section bytes */
    783   for (u32 i = 0; i < nsecs; ++i) {
    784     MSec* m = &secs[i];
    785     if (m->is_zerofill || !m->size) continue;
    786     kit_writer_seek(w, m->fileoff);
    787     if (m->obj_bytes) {
    788       u32 sz = m->obj_bytes->total;
    789       if (m->patch_bytes) {
    790         kit_writer_write(w, m->patch_bytes, sz);
    791       } else {
    792         u8* tmp = (u8*)h->alloc(h, sz ? sz : 1, 1);
    793         if (sz) buf_flatten(m->obj_bytes, tmp);
    794         kit_writer_write(w, tmp, sz);
    795         h->free(h, tmp, sz ? sz : 1);
    796       }
    797     }
    798   }
    799 
    800   /* reloc tables */
    801   for (u32 i = 0; i < nsecs; ++i) {
    802     MSec* m = &secs[i];
    803     if (!m->nreloc) continue;
    804     kit_writer_seek(w, m->reloff);
    805     kit_writer_write(w, m->relocs, (size_t)m->nreloc * MACHO_RELOC_SIZE);
    806   }
    807 
    808   /* symtab */
    809   kit_writer_seek(w, symoff);
    810   for (u32 i = 0; i < nmsyms; ++i) {
    811     const MSym* ms = &msyms[i];
    812     u8 entry[MACHO_NLIST64_SIZE];
    813     wr_u32_le(entry + 0, ms->strx);
    814     entry[4] = ms->n_type;
    815     entry[5] = ms->n_sect;
    816     wr_u16_le(entry + 6, ms->n_desc);
    817     wr_u64_le(entry + 8, ms->n_value);
    818     kit_writer_write(w, entry, MACHO_NLIST64_SIZE);
    819   }
    820 
    821   /* strtab — kit_writer_write consumes synchronously and nothing
    822    * appends to the strtab after this point, so write its contiguous
    823    * buffer directly (no flatten copy) and free it afterwards. */
    824   kit_writer_seek(w, stroff);
    825   if (strtab_size) kit_writer_write(w, obj_strtab_data(&strtab), strtab_size);
    826   obj_strtab_fini(&strtab);
    827 }