kit

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

read.c (39627B)


      1 /* Mach-O MH_OBJECT reader.  Parses a 64-bit little-endian relocatable
      2  * object back into a fresh ObjBuilder.  The post-finalize ObjBuilder
      3  * shape is the canonical superset of the writer's input:
      4  * read_macho of an emit_macho output produces an ObjBuilder
      5  * shape-equivalent to the writer's input, modulo the synthesized
      6  * "__SEG,__sect"-form section names.
      7  *
      8  * Scope: AArch64 little-endian.  MH_OBJECT parses to the section/symbol/
      9  * reloc view; MH_EXECUTE / MH_DYLIB additionally get the linked-image view
     10  * (read_macho_image: segments, dylibs, entry, dynamic symbols + relocs).
     11  * read_macho_dso remains the linker's DSO-only input path.  Other archs /
     12  * endianness produce a compiler_panic with a diagnostic. */
     13 
     14 #include <stdlib.h>
     15 #include <string.h>
     16 
     17 #include "core/arena.h"
     18 #include "core/bytes.h"
     19 #include "core/heap.h"
     20 #include "core/pool.h"
     21 #include "core/slice.h"
     22 #include "core/util.h"
     23 #include "obj/format.h"
     24 #include "obj/macho/macho.h"
     25 
     26 /* ---- mach-section scratch struct ---- */
     27 
     28 typedef struct MSecRec {
     29   char segname[16];
     30   char sectname[16];
     31   u32 seg_len;
     32   u32 sect_len;
     33   u64 addr;
     34   u64 size;
     35   u32 fileoff;
     36   u32 align_log2;
     37   u32 reloff;
     38   u32 nreloc;
     39   u32 flags;
     40   u32 reserved2;
     41   ObjSecId obj_sec; /* assigned in pass 1 */
     42 } MSecRec;
     43 
     44 typedef struct MAtomCand {
     45   ObjSecId sec;
     46   ObjSymId sym;
     47   u32 offset;
     48   u32 flags;
     49 } MAtomCand;
     50 
     51 static int matom_cand_cmp(const void* av, const void* bv) {
     52   const MAtomCand* a = (const MAtomCand*)av;
     53   const MAtomCand* b = (const MAtomCand*)bv;
     54   if (a->sec < b->sec) return -1;
     55   if (a->sec > b->sec) return 1;
     56   if (a->offset < b->offset) return -1;
     57   if (a->offset > b->offset) return 1;
     58   if (a->sym < b->sym) return -1;
     59   if (a->sym > b->sym) return 1;
     60   return 0;
     61 }
     62 
     63 static u32 fixed16_len(const char* s) {
     64   u32 n = 0;
     65   while (n < 16 && s[n] != 0) ++n;
     66   return n;
     67 }
     68 
     69 static u16 sec_kind_from_seg_sect(const char* segname, u32 seg_len,
     70                                   const char* sectname, u32 sect_len,
     71                                   u32 flags) {
     72   u32 stype = flags & SECTION_TYPE;
     73   if (stype == S_ZEROFILL || stype == S_THREAD_LOCAL_ZEROFILL) return SEC_BSS;
     74   if (flags & S_ATTR_PURE_INSTRUCTIONS) return SEC_TEXT;
     75 
     76   if (seg_len == 7 && memcmp(segname, "__DWARF", 7) == 0) return SEC_DEBUG;
     77   if (seg_len == 6 && memcmp(segname, "__TEXT", 6) == 0) {
     78     if (sect_len == 6 && memcmp(sectname, "__text", 6) == 0) return SEC_TEXT;
     79     return SEC_RODATA; /* __const, __cstring, ... */
     80   }
     81   if (seg_len == 6 && memcmp(segname, "__DATA", 6) == 0) {
     82     if (sect_len == 5 && memcmp(sectname, "__bss", 5) == 0) return SEC_BSS;
     83     return SEC_DATA;
     84   }
     85   return SEC_OTHER;
     86 }
     87 
     88 static u16 sec_flags_from(u32 mflags, u16 sec_kind) {
     89   u16 f = 0;
     90   if (sec_kind == SEC_TEXT || (mflags & S_ATTR_PURE_INSTRUCTIONS)) {
     91     f |= SF_ALLOC | SF_EXEC;
     92   } else if (sec_kind == SEC_RODATA) {
     93     f |= SF_ALLOC;
     94   } else if (sec_kind == SEC_DATA || sec_kind == SEC_BSS) {
     95     f |= SF_ALLOC | SF_WRITE;
     96   }
     97   u32 stype = mflags & SECTION_TYPE;
     98   if (stype == S_THREAD_LOCAL_REGULAR || stype == S_THREAD_LOCAL_ZEROFILL ||
     99       stype == S_THREAD_LOCAL_VARIABLES) {
    100     f |= SF_TLS;
    101   }
    102   if (stype == S_CSTRING_LITERALS) {
    103     f |= SF_MERGE | SF_STRINGS;
    104   }
    105   return f;
    106 }
    107 
    108 static u16 sec_sem_from(u32 mflags, u16 sec_kind) {
    109   u32 stype = mflags & SECTION_TYPE;
    110   if (stype == S_ZEROFILL || stype == S_THREAD_LOCAL_ZEROFILL ||
    111       sec_kind == SEC_BSS) {
    112     return SSEM_NOBITS;
    113   }
    114   if (stype == S_MOD_INIT_FUNC_POINTERS) return SSEM_INIT_ARRAY;
    115   if (stype == S_MOD_TERM_FUNC_POINTERS) return SSEM_FINI_ARRAY;
    116   return SSEM_PROGBITS;
    117 }
    118 
    119 /* Intern a Mach-O lc_str (NUL-terminated string embedded inside a load
    120  * command at `cmd_pos + str_off`, bounded by the command's cmdsize).
    121  * Returns 0 if the offset/string is malformed. */
    122 static Sym macho_lc_str(Compiler* c, const u8* data, u64 cmd_pos, u32 cmdsize,
    123                         u32 str_off) {
    124   if (str_off < 8 || str_off >= cmdsize) return 0;
    125   const char* p = (const char*)(data + cmd_pos + str_off);
    126   u32 maxlen = cmdsize - str_off;
    127   u32 nlen = 0;
    128   while (nlen < maxlen && p[nlen]) ++nlen;
    129   if (!nlen) return 0;
    130   return pool_intern_slice(c->global, (Slice){.s = p, .len = nlen});
    131 }
    132 
    133 /* ---- read_macho_image ----
    134  *
    135  * Linked-image (MH_EXECUTE / MH_DYLIB) view, the Mach-O peer of
    136  * read_elf_image. Walks the load commands a second time to populate the
    137  * ObjImage: LC_SEGMENT_64 -> segments (+ __TEXT base), LC_LOAD_DYLINKER ->
    138  * interp, LC_ID_DYLIB -> soname, LC_LOAD_DYLIB/WEAK/REEXPORT -> deps,
    139  * LC_RPATH -> rpaths, LC_MAIN/LC_UNIXTHREAD -> entry, the LC_SYMTAB external
    140  * nlist entries -> dynamic symbols, and LC_DYLD_CHAINED_FIXUPS binds/rebases
    141  * -> dynamic relocations. The section / symbol / reloc views are parsed by
    142  * read_macho's normal passes; this adds the orthogonal image dimension.
    143  * Lenient: a malformed sub-table is skipped rather than panicked, so a
    144  * partially-damaged image still yields a useful inspection.
    145  *
    146  * `msecs`/`nmsecs` carry the section table read in read_macho's pass 1 so a
    147  * defined dynamic symbol's n_sect maps back to its ObjSecId. */
    148 static void read_macho_image(Compiler* c, ObjBuilder* ob, const u8* data,
    149                              size_t len, u32 filetype, u32 cputype,
    150                              const MSecRec* msecs, u32 nmsecs) {
    151   ObjImage* im =
    152       obj_image_ensure(ob, filetype == MH_DYLIB ? OBJ_KIND_DYN : OBJ_KIND_EXEC);
    153   if (!im)
    154     compiler_panic(c, SRCLOC_NONE, "read_macho: obj_image_ensure failed");
    155 
    156   u32 ncmds = rd_u32_le(data + 16);
    157   u32 sizeofcmds = rd_u32_le(data + 20);
    158 
    159   /* Per-segment (vmaddr, file_off) recorded for chained-fixup vaddr
    160    * resolution below; sized to ncmds (segments are a subset of commands). */
    161   u64* seg_vaddr = arena_array(c->scratch, u64, ncmds ? ncmds : 1);
    162   u64* seg_fileoff = arena_array(c->scratch, u64, ncmds ? ncmds : 1);
    163   u32 nseg = 0;
    164 
    165   int have_text = 0;
    166   u64 text_vmaddr = 0;
    167   int have_main = 0;
    168   u64 main_entryoff = 0;
    169   u32 symoff = 0, nsyms = 0, stroff = 0, strsize = 0;
    170   u32 cf_off = 0, cf_size = 0;
    171 
    172   u64 pos = MACHO_HDR64_SIZE;
    173   u64 end = pos + sizeofcmds;
    174   for (u32 ci = 0; ci < ncmds && pos + 8 <= end; ++ci) {
    175     u32 cmd = rd_u32_le(data + pos);
    176     u32 cmdsize = rd_u32_le(data + pos + 4);
    177     if (cmdsize < 8 || pos + cmdsize > end) break;
    178 
    179     /* Raw load-command view (escape hatch): one entry per LC_* command,
    180      * carrying its file offset and on-disk size. */
    181     {
    182       ObjImageRaw r;
    183       r.tag = cmd;
    184       r.value = pos;
    185       r.extra = cmdsize;
    186       obj_image_add_raw(im, &r);
    187     }
    188 
    189     if (cmd == LC_SEGMENT_64 && cmdsize >= MACHO_SEGCMD64_SIZE) {
    190       const char* segname = (const char*)(data + pos + 8);
    191       u32 seg_len = fixed16_len(segname);
    192       u64 vmaddr = rd_u64_le(data + pos + 24);
    193       u64 vmsize = rd_u64_le(data + pos + 32);
    194       u64 fileoff = rd_u64_le(data + pos + 40);
    195       u64 filesize = rd_u64_le(data + pos + 48);
    196       u32 initprot = rd_u32_le(data + pos + 60);
    197       ObjSegment seg;
    198       seg.name = seg_len ? pool_intern_slice(
    199                                c->global, (Slice){.s = segname, .len = seg_len})
    200                          : 0;
    201       seg.vaddr = vmaddr;
    202       seg.paddr = vmaddr;
    203       seg.vsize = vmsize;
    204       seg.file_off = fileoff;
    205       seg.file_size = filesize;
    206       /* VM_PROT_* bits differ from OBJ_SEG_* — remap explicitly. */
    207       seg.perms = ((initprot & VM_PROT_READ) ? OBJ_SEG_R : 0) |
    208                   ((initprot & VM_PROT_WRITE) ? OBJ_SEG_W : 0) |
    209                   ((initprot & VM_PROT_EXECUTE) ? OBJ_SEG_X : 0);
    210       seg.align = 1; /* Mach-O segments don't carry an explicit p_align */
    211       obj_image_add_segment(im, &seg);
    212 
    213       seg_vaddr[nseg] = vmaddr;
    214       seg_fileoff[nseg] = fileoff;
    215       ++nseg;
    216       if (!have_text && seg_len == 6 && memcmp(segname, "__TEXT", 6) == 0) {
    217         have_text = 1;
    218         text_vmaddr = vmaddr;
    219       }
    220     } else if (cmd == LC_LOAD_DYLINKER) {
    221       Sym s = macho_lc_str(c, data, pos, cmdsize, rd_u32_le(data + pos + 8));
    222       if (s) obj_image_set_interp(im, s);
    223     } else if (cmd == LC_ID_DYLIB) {
    224       Sym s = macho_lc_str(c, data, pos, cmdsize, rd_u32_le(data + pos + 8));
    225       if (s) obj_image_set_soname(im, s);
    226     } else if (cmd == LC_LOAD_DYLIB || cmd == LC_LOAD_WEAK_DYLIB ||
    227                cmd == LC_REEXPORT_DYLIB) {
    228       Sym s = macho_lc_str(c, data, pos, cmdsize, rd_u32_le(data + pos + 8));
    229       if (s) {
    230         ObjImageDep d;
    231         d.name = s;
    232         d.imports = NULL;
    233         d.nimports = 0;
    234         obj_image_add_dep(im, &d);
    235       }
    236     } else if (cmd == LC_RPATH) {
    237       Sym s = macho_lc_str(c, data, pos, cmdsize, rd_u32_le(data + pos + 8));
    238       if (s) obj_image_add_rpath(im, s);
    239     } else if (cmd == LC_MAIN && cmdsize >= 16) {
    240       have_main = 1;
    241       main_entryoff = rd_u64_le(data + pos + 8);
    242     } else if (cmd == LC_UNIXTHREAD && cmdsize >= 16 && !have_main) {
    243       /* thread_command: flavor (u32) + count (u32) + register state. Pull
    244        * the program counter out of the arch's state. */
    245       u32 flavor = rd_u32_le(data + pos + 8);
    246       u64 pc_off = 0;
    247       int have_pc = 0;
    248       if (cputype == CPU_TYPE_ARM64 && flavor == 6 /* ARM_THREAD_STATE64 */) {
    249         pc_off = pos + 16 + 32u * 8u; /* x0..x28,fp,lr,sp,pc */
    250         have_pc = 1;
    251       } else if (cputype == CPU_TYPE_X86_64 &&
    252                  flavor == 4 /* x86_THREAD_STATE64 */) {
    253         pc_off = pos + 16 + 16u * 8u; /* rax..r15, then rip */
    254         have_pc = 1;
    255       }
    256       if (have_pc && pc_off + 8 <= pos + cmdsize)
    257         obj_image_set_entry(im, rd_u64_le(data + pc_off));
    258     } else if (cmd == LC_SYMTAB && cmdsize >= MACHO_SYMTAB_CMD_SIZE) {
    259       symoff = rd_u32_le(data + pos + 8);
    260       nsyms = rd_u32_le(data + pos + 12);
    261       stroff = rd_u32_le(data + pos + 16);
    262       strsize = rd_u32_le(data + pos + 20);
    263     } else if (cmd == LC_DYLD_CHAINED_FIXUPS && cmdsize >= 16) {
    264       cf_off = rd_u32_le(data + pos + 8);
    265       cf_size = rd_u32_le(data + pos + 12);
    266     }
    267     pos += cmdsize;
    268   }
    269 
    270   if (have_text) obj_image_set_base(im, text_vmaddr);
    271   /* LC_MAIN entryoff is a file offset within __TEXT (which maps file 0 to
    272    * its vmaddr); the entry vaddr is __TEXT base + entryoff. */
    273   if (have_main && have_text)
    274     obj_image_set_entry(im, text_vmaddr + main_entryoff);
    275 
    276   /* LC_SYMTAB external nlist entries -> dynamic symbols (Mach-O's analog of
    277    * .dynsym: the dynamically-visible exports and undefined imports). */
    278   if (nsyms && stroff + (u64)strsize <= len &&
    279       symoff + (u64)nsyms * MACHO_NLIST64_SIZE <= len) {
    280     const u8* strtab = data + stroff;
    281     const u8* sbase = data + symoff;
    282     for (u32 i = 0; i < nsyms; ++i) {
    283       const u8* p = sbase + (u64)i * MACHO_NLIST64_SIZE;
    284       u32 strx = rd_u32_le(p + 0);
    285       u8 n_type = p[4];
    286       u8 n_sect = p[5];
    287       u16 n_desc = rd_u16_le(p + 6);
    288       u64 n_value = rd_u64_le(p + 8);
    289       if (n_type & N_STAB) continue;   /* debug stab, not dynamic */
    290       if (!(n_type & N_EXT)) continue; /* locals aren't dynamic */
    291       if (strx >= strsize) continue;
    292       const char* nm = (const char*)(strtab + strx);
    293       u32 nlen = 0;
    294       while (strx + nlen < strsize && nm[nlen]) ++nlen;
    295       if (!nlen) continue;
    296 
    297       u8 type_field = (u8)(n_type & N_TYPE);
    298       ObjImageSym ds;
    299       memset(&ds, 0, sizeof ds);
    300       ds.version = 0; /* Mach-O has no ELF-style symbol versioning */
    301       ds.name = pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen});
    302       ds.bind = (n_desc & (N_WEAK_DEF | N_WEAK_REF)) ? SB_WEAK : SB_GLOBAL;
    303       ds.value = (type_field == N_SECT || type_field == N_ABS) ? n_value : 0;
    304       ds.size = 0;
    305       if (type_field == N_SECT && n_sect >= 1 && n_sect <= nmsecs) {
    306         ds.section = msecs[n_sect - 1].obj_sec;
    307         ds.kind = (msecs[n_sect - 1].flags & S_ATTR_PURE_INSTRUCTIONS) ? SK_FUNC
    308                                                                        : SK_OBJ;
    309       } else {
    310         ds.section = OBJ_SEC_NONE; /* undefined import / absolute */
    311         ds.kind = SK_NOTYPE;
    312       }
    313       obj_image_add_dynsym(im, &ds);
    314     }
    315   }
    316 
    317   /* LC_DYLD_CHAINED_FIXUPS binds/rebases -> dynamic relocations. */
    318   if (cf_size >= 28 && (u64)cf_off + cf_size <= len) {
    319     const u8* cf = data + cf_off;
    320     u32 starts_offset = rd_u32_le(cf + 4);
    321     u32 imports_offset = rd_u32_le(cf + 8);
    322     u32 symbols_offset = rd_u32_le(cf + 12);
    323     u32 imports_count = rd_u32_le(cf + 16);
    324     u32 imports_format = rd_u32_le(cf + 20);
    325     u32 relative_kind =
    326         (cputype == CPU_TYPE_X86_64) ? R_X64_RELATIVE : R_AARCH64_RELATIVE;
    327 
    328     /* Import symbol names, indexed by 0-based bind ordinal. */
    329     Sym* imp_names =
    330         arena_zarray(c->scratch, Sym, imports_count ? imports_count : 1);
    331     if (imports_format == DYLD_CHAINED_IMPORT &&
    332         (u64)imports_offset + (u64)imports_count * 4u <= cf_size) {
    333       for (u32 i = 0; i < imports_count; ++i) {
    334         u32 packed = rd_u32_le(cf + imports_offset + i * 4u);
    335         u32 name_off = (packed >> 9) & 0x7fffffu;
    336         u64 so = (u64)symbols_offset + name_off;
    337         if (so >= cf_size) continue;
    338         const char* nm = (const char*)(cf + so);
    339         u32 maxn = (u32)(cf_size - so);
    340         u32 nlen = 0;
    341         while (nlen < maxn && nm[nlen]) ++nlen;
    342         if (nlen)
    343           imp_names[i] =
    344               pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen});
    345       }
    346     }
    347 
    348     if ((u64)starts_offset + 4u <= cf_size) {
    349       const u8* sib = cf + starts_offset;
    350       u32 seg_count = rd_u32_le(sib + 0);
    351       for (u32 si = 0; si < seg_count; ++si) {
    352         if ((u64)starts_offset + 4u + (u64)si * 4u + 4u > cf_size) break;
    353         u32 seg_info_offset = rd_u32_le(sib + 4 + si * 4u);
    354         if (!seg_info_offset) continue;
    355         if ((u64)starts_offset + seg_info_offset + 22u > cf_size) continue;
    356         const u8* sis = cf + starts_offset + seg_info_offset;
    357         u16 pointer_format = rd_u16_le(sis + 6);
    358         u64 segment_offset = rd_u64_le(sis + 8); /* file offset of segment */
    359         u16 page_count = rd_u16_le(sis + 20);
    360         /* Only the DYLD_CHAINED_PTR_64 family shares the bit layout below. */
    361         if (pointer_format != DYLD_CHAINED_PTR_64 && pointer_format != 6u)
    362           continue;
    363         u16 page_size = rd_u16_le(sis + 4);
    364         if (!page_size) continue;
    365         /* Resolve this segment's vmaddr from its file offset. */
    366         u64 seg_va = 0;
    367         int found_seg = 0;
    368         for (u32 k = 0; k < nseg; ++k) {
    369           if (seg_fileoff[k] == segment_offset) {
    370             seg_va = seg_vaddr[k];
    371             found_seg = 1;
    372             break;
    373           }
    374         }
    375         if (!found_seg) continue;
    376         for (u32 pg = 0; pg < page_count; ++pg) {
    377           u64 ps_pos = (u64)starts_offset + seg_info_offset + 22u + pg * 2u;
    378           if (ps_pos + 2u > cf_size) break;
    379           u16 ps = rd_u16_le(cf + ps_pos);
    380           if (ps == 0xFFFFu) continue;
    381           u32 cur = ps;
    382           for (;;) {
    383             u64 file_loc = segment_offset + (u64)pg * page_size + cur;
    384             if (file_loc + 8u > len) break;
    385             u64 v = rd_u64_le(data + file_loc);
    386             u64 vaddr = seg_va + (u64)pg * page_size + cur;
    387             int is_bind = (int)((v >> 63) & 1u);
    388             ObjImageReloc dr;
    389             dr.section = OBJ_SEC_NONE;
    390             dr.offset = vaddr;
    391             if (is_bind) {
    392               u32 ordinal = (u32)(v & 0xffffffu);
    393               dr.sym_name = (ordinal < imports_count) ? imp_names[ordinal] : 0;
    394               dr.addend = (i64)((v >> 24) & 0xffu);
    395               dr.kind = R_ABS64;
    396             } else {
    397               dr.sym_name = 0;
    398               dr.addend = (i64)(v & (((u64)1 << 36) - 1u));
    399               dr.kind = (RelocKind)relative_kind;
    400             }
    401             obj_image_add_dynreloc(im, &dr);
    402             u32 next = (u32)((v >> 51) & 0xfffu);
    403             if (!next) break;
    404             cur += next * 4u;
    405             if (cur >= page_size) break;
    406           }
    407         }
    408       }
    409     }
    410   }
    411 }
    412 
    413 ObjBuilder* read_macho(Compiler* c, const char* name, const u8* data,
    414                        size_t len) {
    415   (void)name;
    416   if (len < MACHO_HDR64_SIZE)
    417     compiler_panic(c, SRCLOC_NONE, "read_macho: input shorter than header");
    418 
    419   u32 magic = rd_u32_le(data + 0);
    420   if (magic != MH_MAGIC_64)
    421     compiler_panic(c, SRCLOC_NONE, "read_macho: bad magic 0x%x", magic);
    422 
    423   u32 cputype = rd_u32_le(data + 4);
    424   const ObjFormatImpl* fmt = obj_format_lookup(KIT_OBJ_MACHO);
    425   const ObjMachoArchOps* macho =
    426       fmt && fmt->macho_cputype ? fmt->macho_cputype(cputype) : NULL;
    427   u32 filetype = rd_u32_le(data + 12);
    428   u32 ncmds = rd_u32_le(data + 16);
    429   u32 sizeofcmds = rd_u32_le(data + 20);
    430   u32 mh_flags = rd_u32_le(data + 24);
    431 
    432   if (!macho || !macho->reloc_decode)
    433     compiler_panic(c, SRCLOC_NONE, "read_macho: unsupported cputype 0x%x",
    434                    cputype);
    435   /* MH_OBJECT parses to the section/symbol/reloc view only. MH_EXECUTE /
    436    * MH_DYLIB additionally get the linked-image view (read_macho_image, at
    437    * the end); their sections still parse through the same passes. */
    438   if (filetype != MH_OBJECT && filetype != MH_EXECUTE && filetype != MH_DYLIB)
    439     compiler_panic(c, SRCLOC_NONE,
    440                    "read_macho: unsupported filetype %u (expected MH_OBJECT, "
    441                    "MH_EXECUTE, or MH_DYLIB)",
    442                    filetype);
    443 
    444   if ((u64)MACHO_HDR64_SIZE + sizeofcmds > len)
    445     compiler_panic(c, SRCLOC_NONE, "read_macho: load commands exceed file");
    446 
    447   /* ---- pass 1: walk load commands, collect sections, symtab cmd. */
    448   MSecRec* msecs = NULL;
    449   u32 nmsecs = 0;
    450   u32 symoff = 0, nsyms = 0, stroff = 0, strsize = 0;
    451 
    452   u64 pos = MACHO_HDR64_SIZE;
    453   u64 end = pos + sizeofcmds;
    454   for (u32 ci = 0; ci < ncmds && pos + 8 <= end; ++ci) {
    455     u32 cmd = rd_u32_le(data + pos);
    456     u32 cmdsize = rd_u32_le(data + pos + 4);
    457     if (cmdsize < 8 || pos + cmdsize > end)
    458       compiler_panic(c, SRCLOC_NONE, "read_macho: malformed load command");
    459 
    460     if (cmd == LC_SEGMENT_64) {
    461       u32 nsects = rd_u32_le(data + pos + 64);
    462       if (MACHO_SEGCMD64_SIZE + (u64)nsects * MACHO_SECT64_SIZE > cmdsize)
    463         compiler_panic(c, SRCLOC_NONE, "read_macho: segment cmd truncated");
    464       MSecRec* extra = arena_array(c->scratch, MSecRec, nmsecs + nsects);
    465       if (msecs && nmsecs) memcpy(extra, msecs, sizeof(MSecRec) * nmsecs);
    466       msecs = extra;
    467       const u8* sp = data + pos + MACHO_SEGCMD64_SIZE;
    468       for (u32 si = 0; si < nsects; ++si, sp += MACHO_SECT64_SIZE) {
    469         MSecRec* m = &msecs[nmsecs++];
    470         memset(m, 0, sizeof *m);
    471         memcpy(m->sectname, sp + 0, 16);
    472         memcpy(m->segname, sp + 16, 16);
    473         m->seg_len = fixed16_len(m->segname);
    474         m->sect_len = fixed16_len(m->sectname);
    475         m->addr = rd_u64_le(sp + 32);
    476         m->size = rd_u64_le(sp + 40);
    477         m->fileoff = rd_u32_le(sp + 48);
    478         m->align_log2 = rd_u32_le(sp + 52);
    479         m->reloff = rd_u32_le(sp + 56);
    480         m->nreloc = rd_u32_le(sp + 60);
    481         m->flags = rd_u32_le(sp + 64);
    482         m->reserved2 = rd_u32_le(sp + 72);
    483       }
    484     } else if (cmd == LC_SYMTAB) {
    485       symoff = rd_u32_le(data + pos + 8);
    486       nsyms = rd_u32_le(data + pos + 12);
    487       stroff = rd_u32_le(data + pos + 16);
    488       strsize = rd_u32_le(data + pos + 20);
    489     }
    490     pos += cmdsize;
    491   }
    492 
    493   if (stroff + (u64)strsize > len)
    494     compiler_panic(c, SRCLOC_NONE, "read_macho: string table out of range");
    495   if (symoff + (u64)nsyms * MACHO_NLIST64_SIZE > len)
    496     compiler_panic(c, SRCLOC_NONE, "read_macho: symbol table out of range");
    497   const u8* strtab = data + stroff;
    498 
    499   ObjBuilder* ob = obj_new(c);
    500   if (!ob) compiler_panic(c, SRCLOC_NONE, "read_macho: obj_new failed");
    501   obj_reserve_symbols(ob, nsyms); /* skip the 256->nsyms resize cascade */
    502 
    503   /* ---- pass 2: create ObjSecs and copy bytes. */
    504   for (u32 i = 0; i < nmsecs; ++i) {
    505     MSecRec* m = &msecs[i];
    506     /* Build "__SEG,__sect"-form name; matches what emit_macho would
    507      * round-trip back out. */
    508     char nmbuf[34];
    509     u32 nlen = 0;
    510     memcpy(nmbuf + nlen, m->segname, m->seg_len);
    511     nlen += m->seg_len;
    512     nmbuf[nlen++] = ',';
    513     memcpy(nmbuf + nlen, m->sectname, m->sect_len);
    514     nlen += m->sect_len;
    515     Sym sn = pool_intern_slice(c->global, (Slice){.s = nmbuf, .len = nlen});
    516 
    517     u16 kind = sec_kind_from_seg_sect(m->segname, m->seg_len, m->sectname,
    518                                       m->sect_len, m->flags);
    519     u16 flags = sec_flags_from(m->flags, kind);
    520     u16 sem = sec_sem_from(m->flags, kind);
    521     u32 align = 1u << (m->align_log2 & 31);
    522 
    523     ObjSecId id = obj_section_ex(ob, sn, (SecKind)kind, (SecSem)sem, flags,
    524                                  align, m->reserved2, 0, 0);
    525     if (id == OBJ_SEC_NONE)
    526       compiler_panic(c, SRCLOC_NONE, "read_macho: obj_section_ex failed");
    527 
    528     /* Preserve the raw mach section.flags so emit_macho can write back
    529      * the same S_TYPE / S_ATTR_* bits. */
    530     obj_section_set_ext(ob, id, OBJ_EXT_MACHO, m->flags, 0);
    531 
    532     if (sem == SSEM_NOBITS) {
    533       obj_reserve_bss(ob, id, (u32)m->size, align);
    534     } else if (m->size) {
    535       if (m->fileoff + m->size > len)
    536         compiler_panic(c, SRCLOC_NONE,
    537                        "read_macho: section bytes out of range");
    538       obj_write(ob, id, data + m->fileoff, (size_t)m->size);
    539     }
    540     m->obj_sec = id;
    541   }
    542 
    543   /* ---- pass 3: parse symbol table.  Two-pass strategy: first pass
    544    *              creates undefs (so relocations can refer to them), second
    545    *              pass creates defined locals/extdefs.  Both write into
    546    *              mach_idx -> ObjSymId so reloc resolution works. */
    547   ObjSymId* sym_macho_to_obj =
    548       arena_zarray(c->scratch, ObjSymId, nsyms ? nsyms : 1);
    549   MAtomCand* atom_cands =
    550       arena_zarray(c->scratch, MAtomCand, nsyms ? nsyms : 1);
    551   u32 natom_cands = 0;
    552 
    553   const u8* sbase = data + symoff;
    554   for (u32 i = 0; i < nsyms; ++i) {
    555     const u8* p = sbase + (u64)i * MACHO_NLIST64_SIZE;
    556     u32 strx = rd_u32_le(p + 0);
    557     u8 n_type = p[4];
    558     u8 n_sect = p[5];
    559     u16 n_desc = rd_u16_le(p + 6);
    560     u64 n_value = rd_u64_le(p + 8);
    561 
    562     const char* nm = "";
    563     u32 nlen = 0;
    564     if (strx < strsize) {
    565       nm = (const char*)(strtab + strx);
    566       while (strx + nlen < strsize && nm[nlen]) ++nlen;
    567     }
    568     /* Mach-O names round-trip verbatim — the leading `_` Apple
    569      * toolchains apply to C symbols is part of the on-disk name as
    570      * far as ObjBuilder is concerned.  Name-canonicalization (the
    571      * `test_main` ↔ `_test_main` mapping for API callers) happens
    572      * one layer up at the linker API boundary (link_c_name_intern
    573      * in link.c); the on-disk shape stays byte-for-byte stable. */
    574     Sym sn =
    575         nlen ? pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen}) : 0;
    576 
    577     u8 type_field = (u8)(n_type & N_TYPE);
    578     u8 ext = (u8)(n_type & N_EXT);
    579     u8 pext = (u8)(n_type & N_PEXT);
    580 
    581     u16 bind = ext ? SB_GLOBAL : SB_LOCAL;
    582     /* Weak DEFs (defined symbols) carry N_WEAK_DEF; weak REFs (undef
    583      * `__attribute__((weak))` references) carry N_WEAK_REF. Either
    584      * one collapses to SB_WEAK in the kit model. */
    585     if (ext && (n_desc & (N_WEAK_DEF | N_WEAK_REF))) bind = SB_WEAK;
    586     u8 vis = pext ? SV_HIDDEN : SV_DEFAULT;
    587 
    588     u16 kind;
    589     ObjSecId sec_id = OBJ_SEC_NONE;
    590     u64 value = 0;
    591     u64 size = 0;
    592     u64 cmnalign = 0;
    593 
    594     if (type_field == N_UNDF) {
    595       if (ext && n_value != 0) {
    596         /* Common: n_value is size, n_desc encodes log2(align) in
    597          * GET_COMM_ALIGN bits. */
    598         kind = SK_COMMON;
    599         value = 0;
    600         size = n_value;
    601         u32 la = (u32)((n_desc >> 8) & 0xf);
    602         cmnalign = 1u << la;
    603       } else {
    604         kind = SK_UNDEF;
    605       }
    606     } else if (type_field == N_ABS) {
    607       kind = SK_ABS;
    608       value = n_value;
    609     } else if (type_field == N_SECT) {
    610       if (n_sect == 0 || n_sect > nmsecs) {
    611         kind = SK_NOTYPE;
    612       } else {
    613         sec_id = msecs[n_sect - 1].obj_sec;
    614         /* MH_OBJECT: the obj model and the linker treat an input
    615          * symbol's value as a section-local offset, and a relocatable
    616          * .o's sections carry non-zero layout addrs, so subtract the
    617          * section base. Linked images (MH_EXECUTE/MH_DYLIB) keep the
    618          * absolute n_value so nm / objdump -t / size / addr2line report
    619          * real vaddrs — matching the ELF reader, whose st_value is
    620          * already absolute for images. */
    621         if (filetype == MH_OBJECT) {
    622           u64 base = msecs[n_sect - 1].addr;
    623           value = (n_value >= base) ? (n_value - base) : 0;
    624         } else {
    625           value = n_value;
    626         }
    627         kind = (msecs[n_sect - 1].flags & S_ATTR_PURE_INSTRUCTIONS) ? SK_FUNC
    628                                                                     : SK_OBJ;
    629       }
    630     } else {
    631       kind = SK_NOTYPE;
    632     }
    633 
    634     ObjSymId id = obj_symbol_ex(ob, sn, (SymBind)bind, (SymVis)vis,
    635                                 (SymKind)kind, sec_id, value, size, cmnalign);
    636     obj_sym_mark_referenced(ob, id);
    637     if ((mh_flags & MH_SUBSECTIONS_VIA_SYMBOLS) && type_field == N_SECT &&
    638         sec_id != OBJ_SEC_NONE && !(n_desc & N_ALT_ENTRY)) {
    639       MAtomCand* ac = &atom_cands[natom_cands++];
    640       ac->sec = sec_id;
    641       ac->sym = id;
    642       ac->offset = (u32)value;
    643       if ((n_desc & N_NO_DEAD_STRIP) ||
    644           (n_sect != 0 && n_sect <= nmsecs &&
    645            (msecs[n_sect - 1].flags & S_ATTR_NO_DEAD_STRIP))) {
    646         ac->flags |= OBJ_ATOM_RETAIN;
    647       }
    648     }
    649     /* n_desc carries Mach-O attribute bits beyond what bind/vis/kind
    650      * model — N_NO_DEAD_STRIP, N_REF_TO_WEAK, N_ARM_THUMB_DEF, etc.
    651      * Mask off the bits we already round-trip via bind (N_WEAK_DEF /
    652      * N_WEAK_REF) and the alignment field for commons (which lives
    653      * in cmnalign), then stash the remainder so emit_macho can OR it
    654      * back in. */
    655     u16 desc_pass = n_desc;
    656     desc_pass &= (u16) ~(N_WEAK_DEF | N_WEAK_REF);
    657     if (type_field == N_SECT) desc_pass &= (u16)~N_ALT_ENTRY;
    658     if (kind == SK_COMMON) desc_pass &= 0x00ff; /* drop align field */
    659     if (desc_pass) obj_symbol_set_flags(ob, id, desc_pass);
    660     if (type_field == N_SECT && (n_desc & N_ALT_ENTRY))
    661       obj_symbol_set_atom_subordinate(ob, id, 1);
    662     sym_macho_to_obj[i] = id;
    663   }
    664 
    665   if (mh_flags & MH_SUBSECTIONS_VIA_SYMBOLS) {
    666     if (natom_cands > 1u)
    667       qsort(atom_cands, natom_cands, sizeof(*atom_cands), matom_cand_cmp);
    668     for (u32 i = 0; i < natom_cands; ++i) {
    669       MAtomCand* ac = &atom_cands[i];
    670       const Section* sec = obj_section_get(ob, ac->sec);
    671       u32 end = sec ? ((sec->sem == SSEM_NOBITS || sec->kind == SEC_BSS)
    672                            ? sec->bss_size
    673                            : sec->bytes.total)
    674                     : ac->offset;
    675       if (i + 1u < natom_cands && atom_cands[i + 1u].sec == ac->sec)
    676         end = atom_cands[i + 1u].offset;
    677       if (end >= ac->offset)
    678         obj_atom_define(ob, ac->sec, ac->offset, end - ac->offset, ac->sym,
    679                         ac->flags);
    680     }
    681   }
    682 
    683   /* ---- pass 4: parse per-section relocations into ObjBuilder relocs.
    684    *              Mach-O encodes addends out-of-band as a leading
    685    *              ARM64_RELOC_ADDEND followed by the real reloc; the
    686    *              reader collapses the pair on the way in. */
    687   /* Lazily-populated section-start local symbols, for clang-emitted
    688    * non-extern (section-relative) relocations.  See the r_extern==0
    689    * branch below for the encoding. */
    690   ObjSymId* sec_start_sym =
    691       arena_zarray(c->scratch, ObjSymId, nmsecs ? nmsecs : 1);
    692   for (u32 i = 0; i < nmsecs; ++i) sec_start_sym[i] = OBJ_SYM_NONE;
    693   for (u32 i = 0; i < nmsecs; ++i) {
    694     MSecRec* m = &msecs[i];
    695     if (!m->nreloc) continue;
    696     if (m->reloff + (u64)m->nreloc * MACHO_RELOC_SIZE > len)
    697       compiler_panic(c, SRCLOC_NONE,
    698                      "read_macho: relocation table out of range");
    699     const u8* rp = data + m->reloff;
    700     i64 pending_addend = 0;
    701     int have_pending = 0;
    702     int pending_subtractor = 0;
    703     u32 pending_subtractor_offset = 0;
    704     u32 pending_subtractor_length = 0;
    705     for (u32 j = 0; j < m->nreloc; ++j) {
    706       u32 r_address = rd_u32_le(rp + j * MACHO_RELOC_SIZE);
    707       u32 packed = rd_u32_le(rp + j * MACHO_RELOC_SIZE + 4);
    708       u32 r_symbolnum = packed & 0x00ffffffu;
    709       u32 r_pcrel = (packed >> 24) & 1u;
    710       u32 r_length = (packed >> 25) & 3u;
    711       u32 r_extern = (packed >> 27) & 1u;
    712       u32 r_type = (packed >> 28) & 0xfu;
    713 
    714       /* Decode the entry per-arch: r_type's 4 bits are not arch-unique (e.g.
    715        * value 2 is both X86_64_RELOC_BRANCH and ARM64_RELOC_BRANCH26), so the
    716        * arch hook resolves the kind from (r_type, r_pcrel, r_length) and the
    717        * patched instruction, and classifies the entry's structural role. */
    718       MachoRelocEntry rin;
    719       MachoRelocDecoded rdc;
    720       rin.r_type = r_type;
    721       rin.r_pcrel = r_pcrel;
    722       rin.r_length = r_length;
    723       rin.r_extern = r_extern;
    724       rin.r_symbolnum = r_symbolnum;
    725       rin.r_address = r_address;
    726       if ((u64)m->fileoff + r_address + 4u <= len) {
    727         rin.insn = data + m->fileoff + r_address;
    728         rin.insn_len = 4u;
    729       } else {
    730         rin.insn = NULL;
    731         rin.insn_len = 0u;
    732       }
    733       if (!macho->reloc_decode(&rin, &rdc))
    734         compiler_panic(c, SRCLOC_NONE, "read_macho: unsupported reloc type %u",
    735                        r_type);
    736 
    737       if (rdc.role == MACHO_RELOC_ADDEND) {
    738         pending_addend = rdc.addend;
    739         have_pending = 1;
    740         continue;
    741       }
    742 
    743       u32 kind = rdc.kind;
    744       /* An absolute datum at the same offset/width as a just-seen SUBTRACTOR
    745        * is the addend half of a symbol-difference pair: SUB* + ADD*. */
    746       if (rdc.role == MACHO_RELOC_ABSOLUTE && pending_subtractor &&
    747           pending_subtractor_offset == r_address &&
    748           pending_subtractor_length == r_length) {
    749         kind = (r_length == 3)   ? R_ADD64
    750                : (r_length == 2) ? R_ADD32
    751                : (r_length == 1) ? R_ADD16
    752                                  : R_ADD8;
    753         pending_subtractor = 0;
    754       }
    755 
    756       ObjSymId target = OBJ_SYM_NONE;
    757       i64 inplace_addend_override = 0;
    758       int use_inplace_addend = 0;
    759       if (r_extern) {
    760         if (r_symbolnum < nsyms) target = sym_macho_to_obj[r_symbolnum];
    761         if (!have_pending && rdc.inplace_addend) {
    762           u32 rsz = 1u << r_length;
    763           if ((u64)m->fileoff + r_address + rsz > len)
    764             compiler_panic(c, SRCLOC_NONE,
    765                            "read_macho: extern reloc r_address out of range");
    766           const u8* pv = data + m->fileoff + r_address;
    767           i64 inplace;
    768           /* A PC-relative displacement is signed (rdc.inplace_signed); an
    769            * absolute datum reads as written. The arch decoder's bias undoes any
    770            * PC adjustment baked into the field (x86_64: +width). */
    771           if (r_length == 3)
    772             inplace = (i64)rd_u64_le(pv);
    773           else if (r_length == 2)
    774             inplace = rdc.inplace_signed ? (i64)(i32)rd_u32_le(pv)
    775                                          : (i64)(u64)rd_u32_le(pv);
    776           else if (r_length == 1)
    777             inplace = rdc.inplace_signed ? (i64)(i16)rd_u16_le(pv)
    778                                          : (i64)(u64)rd_u16_le(pv);
    779           else
    780             inplace = rdc.inplace_signed ? (i64)(i8)pv[0] : (i64)(u64)pv[0];
    781           inplace_addend_override = inplace + rdc.inplace_bias;
    782           use_inplace_addend = 1;
    783         }
    784       } else {
    785         /* Section-relative reloc — clang emits these for compact unwind,
    786          * EH frame, and DWARF debug info.  r_symbolnum is the 1-based
    787          * section index; the in-place value at r_address is the absolute
    788          * .o virtual address of the referent.  Synthesize a local
    789          * symbol pointing to the target section's start (lazily, once
    790          * per section) and re-express the reloc as
    791          *   target = sec_start_sym,  addend = inplace - section.addr. */
    792         if (r_symbolnum == 0 || r_symbolnum > nmsecs)
    793           compiler_panic(c, SRCLOC_NONE,
    794                          "read_macho: section-relative reloc references "
    795                          "invalid section index %u",
    796                          r_symbolnum);
    797         u32 sec_idx = r_symbolnum - 1u;
    798         MSecRec* tm = &msecs[sec_idx];
    799         if (sec_start_sym[sec_idx] == OBJ_SYM_NONE) {
    800           /* Build ".Lkit.macho_secstart.<sec_idx>" without snprintf
    801            * (the freestanding build doesn't pull in stdio). */
    802           static const char prefix[] = ".Lkit.macho_secstart.";
    803           char nmbuf[sizeof(prefix) + 10];
    804           u32 nlen = (u32)(sizeof(prefix) - 1);
    805           memcpy(nmbuf, prefix, nlen);
    806           char dec[10];
    807           u32 dn = 0;
    808           u32 v = sec_idx;
    809           do {
    810             dec[dn++] = (char)('0' + (v % 10u));
    811             v /= 10u;
    812           } while (v);
    813           for (u32 k = 0; k < dn; ++k) nmbuf[nlen + k] = dec[dn - 1 - k];
    814           nlen += dn;
    815           Sym sn =
    816               pool_intern_slice(c->global, (Slice){.s = nmbuf, .len = nlen});
    817           u16 sk = (tm->flags & S_ATTR_PURE_INSTRUCTIONS) ? SK_FUNC : SK_OBJ;
    818           sec_start_sym[sec_idx] =
    819               obj_symbol(ob, sn, SB_LOCAL, (SymKind)sk, tm->obj_sec, 0, 0);
    820         }
    821         target = sec_start_sym[sec_idx];
    822         u32 rsz = 1u << r_length;
    823         if ((u64)m->fileoff + r_address + rsz > len)
    824           compiler_panic(c, SRCLOC_NONE,
    825                          "read_macho: non-extern reloc r_address out of range");
    826         u64 inplace;
    827         const u8* pv = data + m->fileoff + r_address;
    828         if (r_length == 3)
    829           inplace = rd_u64_le(pv);
    830         else if (r_length == 2)
    831           inplace = (u64)rd_u32_le(pv);
    832         else if (r_length == 1)
    833           inplace = (u64)rd_u16_le(pv);
    834         else
    835           inplace = (u64)pv[0];
    836         inplace_addend_override = (i64)inplace - (i64)tm->addr;
    837         use_inplace_addend = 1;
    838       }
    839 
    840       i64 addend = have_pending
    841                        ? pending_addend
    842                        : (use_inplace_addend ? inplace_addend_override : 0);
    843       int has_explicit = have_pending || use_inplace_addend || addend != 0;
    844       have_pending = 0;
    845       pending_addend = 0;
    846 
    847       obj_reloc_ex(ob, m->obj_sec, r_address, (RelocKind)kind, target, addend,
    848                    has_explicit, 0);
    849       if (rdc.role == MACHO_RELOC_SUBTRACTOR) {
    850         pending_subtractor = 1;
    851         pending_subtractor_offset = r_address;
    852         pending_subtractor_length = r_length;
    853       }
    854     }
    855   }
    856 
    857   /* MH_EXECUTE / MH_DYLIB: attach the linked-image view (segments, dylibs,
    858    * entry, dynamic symbols + relocations). */
    859   if (filetype != MH_OBJECT)
    860     read_macho_image(c, ob, data, len, filetype, cputype, msecs, nmsecs);
    861 
    862   obj_finalize(ob);
    863   return ob;
    864 }
    865 
    866 /* ---- read_macho_dso ----
    867  *
    868  * MH_DYLIB reader.  Walks load commands once to find LC_ID_DYLIB
    869  * (install-name) and LC_SYMTAB (symbol table + string table), then
    870  * emits one defined ObjSym per externally-visible nlist entry.
    871  *
    872  * Like read_elf_dso, the produced ObjBuilder carries no sections /
    873  * relocations / groups — only symbol definitions in OBJ_SEC_NONE.  The
    874  * consumer's resolve_undefs sees these as defined globals and marks the
    875  * matching consumer-side undef as `imported`.  The dylib's own undefs
    876  * (its imports of other dylibs) are filtered: they don't satisfy any
    877  * undef in the consumer. */
    878 
    879 ObjBuilder* read_macho_dso(Compiler* c, const char* name, const u8* data,
    880                            size_t len, Sym* install_name_out) {
    881   (void)name;
    882   if (install_name_out) *install_name_out = 0;
    883   if (len < MACHO_HDR64_SIZE)
    884     compiler_panic(c, SRCLOC_NONE, "read_macho_dso: input shorter than header");
    885 
    886   u32 magic = rd_u32_le(data + 0);
    887   if (magic != MH_MAGIC_64)
    888     compiler_panic(c, SRCLOC_NONE, "read_macho_dso: bad magic 0x%x", magic);
    889 
    890   u32 cputype = rd_u32_le(data + 4);
    891   u32 filetype = rd_u32_le(data + 12);
    892   u32 ncmds = rd_u32_le(data + 16);
    893   u32 sizeofcmds = rd_u32_le(data + 20);
    894 
    895   {
    896     const ObjFormatImpl* fmt = obj_format_lookup(KIT_OBJ_MACHO);
    897     const ObjMachoArchOps* macho =
    898         fmt && fmt->macho_cputype ? fmt->macho_cputype(cputype) : NULL;
    899     if (!macho)
    900       compiler_panic(c, SRCLOC_NONE, "read_macho_dso: unsupported cputype 0x%x",
    901                      cputype);
    902   }
    903   if (filetype != MH_DYLIB && filetype != MH_BUNDLE)
    904     compiler_panic(c, SRCLOC_NONE,
    905                    "read_macho_dso: not MH_DYLIB/MH_BUNDLE (filetype=%u)",
    906                    filetype);
    907   if ((u64)MACHO_HDR64_SIZE + sizeofcmds > len)
    908     compiler_panic(c, SRCLOC_NONE, "read_macho_dso: load commands exceed file");
    909 
    910   u32 symoff = 0, nsyms = 0, stroff = 0, strsize = 0;
    911   Sym install_name = 0;
    912 
    913   u64 pos = MACHO_HDR64_SIZE;
    914   u64 end = pos + sizeofcmds;
    915   for (u32 ci = 0; ci < ncmds && pos + 8 <= end; ++ci) {
    916     u32 cmd = rd_u32_le(data + pos);
    917     u32 cmdsize = rd_u32_le(data + pos + 4);
    918     if (cmdsize < 8 || pos + cmdsize > end)
    919       compiler_panic(c, SRCLOC_NONE, "read_macho_dso: malformed load command");
    920     if (cmd == LC_ID_DYLIB) {
    921       /* dylib_command: cmd, cmdsize, name(lc_str: 4-byte offset within
    922        * the cmd), timestamp, current_version, compat_version. */
    923       if (cmdsize < 24) goto next;
    924       u32 nm_off = rd_u32_le(data + pos + 8);
    925       if (nm_off >= cmdsize) goto next;
    926       const char* p = (const char*)(data + pos + nm_off);
    927       u32 maxlen = cmdsize - nm_off;
    928       u32 nlen = 0;
    929       while (nlen < maxlen && p[nlen]) ++nlen;
    930       if (nlen)
    931         install_name =
    932             pool_intern_slice(c->global, (Slice){.s = p, .len = nlen});
    933     } else if (cmd == LC_SYMTAB) {
    934       symoff = rd_u32_le(data + pos + 8);
    935       nsyms = rd_u32_le(data + pos + 12);
    936       stroff = rd_u32_le(data + pos + 16);
    937       strsize = rd_u32_le(data + pos + 20);
    938     }
    939   next:
    940     pos += cmdsize;
    941   }
    942   if (install_name_out) *install_name_out = install_name;
    943 
    944   if (stroff + (u64)strsize > len)
    945     compiler_panic(c, SRCLOC_NONE, "read_macho_dso: string table out of range");
    946   if (symoff + (u64)nsyms * MACHO_NLIST64_SIZE > len)
    947     compiler_panic(c, SRCLOC_NONE, "read_macho_dso: symbol table out of range");
    948 
    949   ObjBuilder* ob = obj_new(c);
    950   if (!ob) compiler_panic(c, SRCLOC_NONE, "read_macho_dso: obj_new failed");
    951 
    952   const u8* strtab = data + stroff;
    953   const u8* sbase = data + symoff;
    954   for (u32 i = 0; i < nsyms; ++i) {
    955     const u8* p = sbase + (u64)i * MACHO_NLIST64_SIZE;
    956     u32 strx = rd_u32_le(p + 0);
    957     u8 n_type = p[4];
    958     u16 n_desc = rd_u16_le(p + 6);
    959 
    960     u8 type_field = (u8)(n_type & N_TYPE);
    961     u8 ext = (u8)(n_type & N_EXT);
    962     /* Skip non-external (locals) and undef refs (the dylib's own imports). */
    963     if (!ext) continue;
    964     if (type_field == N_UNDF) continue;
    965     /* N_INDR / N_PBUD / N_STAB: skip — not interesting for static link. */
    966     if (n_type & N_STAB) continue;
    967 
    968     if (strx >= strsize) continue;
    969     const char* nm = (const char*)(strtab + strx);
    970     u32 nlen = 0;
    971     while (strx + nlen < strsize && nm[nlen]) ++nlen;
    972     if (!nlen) continue;
    973     Sym sn = pool_intern_slice(c->global, (Slice){.s = nm, .len = nlen});
    974 
    975     SymBind bind = (n_desc & (N_WEAK_DEF | N_WEAK_REF)) ? SB_WEAK : SB_GLOBAL;
    976     SymKind kind = SK_NOTYPE;
    977     /* Mach-O dylib nlist doesn't carry STT_FUNC / STT_OBJECT cleanly —
    978      * default to NOTYPE.  The consuming linker uses dso_export_is_func
    979      * to peek at this for ELF; for Mach-O the `imported` decision flows
    980      * through synthetic __got / __stubs regardless of kind. */
    981     {
    982       ObjSymId did =
    983           obj_symbol_ex(ob, sn, bind, SV_DEFAULT, kind, OBJ_SEC_NONE, 0, 0, 0);
    984       obj_sym_mark_referenced(ob, did);
    985     }
    986   }
    987 
    988   obj_finalize(ob);
    989   return ob;
    990 }