// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "encoding/binary" "fmt" ) // This file emits Mach-O x86-64 objects (MH_OBJECT) from an assembled // Image, in the shape the Darwin assembler produces: one unnamed segment // carrying a __TEXT,__text and a __DATA,__data section laid out back to // back at addresses zero and len(code), a symbol table (locals first, then // exported definitions, then undefined externals) and one relocation entry // per static-symbol reference, of type X86_64_RELOC_SIGNED. // // The image's own address space carries straight over — the data section // starts immediately after the code, and the layout padding already lives // inside Image.Data — so every symbol keeps its image address as its // n_value, and a local (non-external) relocation leaves the displacement // the assembler resolved in place: the linker only adjusts it by the // section's final movement. // Mach-O constants. const ( machoMagic64 = 0xfeedfacf machoCPUamd64 = 0x01000007 // CPU_TYPE_X86_64 machoCPUSubAll = 3 // CPU_SUBTYPE_X86_64_ALL machoObj = 1 // MH_OBJECT machoSegment64 = 0x19 // LC_SEGMENT_64 machoSymtab = 0x2 // LC_SYMTAB machoSectTextFlags = 0x80000400 // S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS nUndf = 0x00 // undefined symbol nSect = 0x0e // defined in section number n_sect nExt = 0x01 // external (exported or undefined-global) bit x8664RelocSigned = 1 ) // MachOObject returns the image as a Mach-O x86-64 relocatable object // (MH_OBJECT), the shape the Darwin toolchain links. Symbol names follow // the same rules as the ELF output. Every static-symbol reference becomes // an X86_64_RELOC_SIGNED relocation: external references against their // undefined symbol, file-local ones against the __DATA section with the // resolved displacement carried in the instruction bytes. func (img *Image) MachOObject() ([]byte, error) { le := binary.LittleEndian // Section ordinals (1-based, as Mach-O numbers them). const ( sectText = 1 sectData = 2 ) // Object address space: code at 0, data immediately after (the layout // padding is already part of img.Data, so image addresses are object // addresses). textAddr := uint64(0) dataAddr := uint64(len(img.Code)) vmsize := dataAddr + uint64(len(img.Data)) // The code, with external displacements primed to addend − 4: the // linker adds the symbol's address to the field as it stands. Local // displacements stay as the assembler resolved them. code := append([]byte(nil), img.Code...) for _, fn := range img.Funcs { for _, r := range fn.Relocs { if r.External { // Prime the field to the addend measured from the patch // site: the assembler records it from the instruction end, // After − Off bytes past the field. copy(code[fn.Offset+r.Off:], le32(r.Addend-int64(r.After-r.Off))) } } } // Symbols: locals first, then exported definitions, then undefined // externals — the order the classic link editor expects. type machoSym struct { name string typ byte sect byte value uint64 } var locals, globals, undefs []machoSym for _, fn := range img.Funcs { s := machoSym{name: objectName(fn.Pkg, fn.Name), typ: nSect, sect: sectText, value: textAddr + uint64(fn.Offset)} if fn.Static { locals = append(locals, s) } else { s.typ |= nExt globals = append(globals, s) } } for _, d := range img.DataSyms { s := machoSym{name: objectName(d.Pkg, d.Name), typ: nSect, sect: sectData, value: dataAddr + uint64(d.Offset)} if d.Static { locals = append(locals, s) } else { s.typ |= nExt globals = append(globals, s) } } for _, name := range img.Externals { undefs = append(undefs, machoSym{name: name, typ: nUndf | nExt}) } syms := append(append(locals, globals...), undefs...) symIdx := map[string]int{} for i, s := range syms { symIdx[s.name] = i } // Relocations, attached to the __text section. type machoReloc struct { addr uint32 symnum uint32 extern bool } var relocs []machoReloc for _, fn := range img.Funcs { for _, r := range fn.Relocs { rel := machoReloc{addr: uint32(fn.Offset + r.Off)} if r.External { idx, ok := symIdx[r.Name] if !ok { return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name) } rel.symnum = uint32(idx) rel.extern = true } else { // Section-relative: r_symbolnum carries the section number // and the resolved displacement stays in the bytes. rel.symnum = sectData } relocs = append(relocs, rel) } } // The string table opens with the conventional " \0". strtab := []byte{' ', 0} strOff := map[string]int{} for _, s := range syms { if _, ok := strOff[s.name]; ok { continue } strOff[s.name] = len(strtab) strtab = append(strtab, s.name...) strtab = append(strtab, 0) } // File layout: header, the two load commands, section data (code, // data), the relocation table, the symbol table, the string table. const ( hdrSize = 32 segCmdSize = 72 + 2*80 // segment command with two sections symCmdSize = 24 ) sizeofcmds := segCmdSize + symCmdSize dataOff := hdrSize + sizeofcmds reloff := dataOff + len(code) + len(img.Data) symoff := reloff + 8*len(relocs) stroff := symoff + 16*len(syms) out := make([]byte, stroff+len(strtab)) // mach_header_64. le.PutUint32(out[0:], machoMagic64) le.PutUint32(out[4:], machoCPUamd64) le.PutUint32(out[8:], machoCPUSubAll) le.PutUint32(out[12:], machoObj) le.PutUint32(out[16:], 2) // ncmds le.PutUint32(out[20:], uint32(sizeofcmds)) le.PutUint32(out[24:], 0) // flags le.PutUint32(out[28:], 0) // reserved // LC_SEGMENT_64 with the two sections. p := hdrSize le.PutUint32(out[p:], machoSegment64) le.PutUint32(out[p+4:], segCmdSize) // segname: the empty string, zero-padded to 16 bytes. le.PutUint64(out[p+8:], 0) le.PutUint64(out[p+16:], 0) le.PutUint64(out[p+24:], 0) // vmaddr le.PutUint64(out[p+32:], vmsize) le.PutUint64(out[p+40:], uint64(dataOff)) le.PutUint64(out[p+48:], vmsize) le.PutUint32(out[p+56:], 7) // maxprot rwx le.PutUint32(out[p+60:], 7) // initprot rwx le.PutUint32(out[p+64:], 2) // nsects le.PutUint32(out[p+68:], 0) // flags // __TEXT,__text s := p + 72 copy(out[s:], "__text") copy(out[s+16:], "__TEXT") le.PutUint64(out[s+32:], textAddr) le.PutUint64(out[s+40:], uint64(len(code))) le.PutUint32(out[s+48:], uint32(dataOff)) le.PutUint32(out[s+52:], 4) // align 2^4 le.PutUint32(out[s+56:], uint32(reloff)) le.PutUint32(out[s+60:], uint32(len(relocs))) le.PutUint32(out[s+64:], machoSectTextFlags) // __DATA,__data s += 80 copy(out[s:], "__data") copy(out[s+16:], "__DATA") le.PutUint64(out[s+32:], dataAddr) le.PutUint64(out[s+40:], uint64(len(img.Data))) le.PutUint32(out[s+48:], uint32(dataOff+len(code))) le.PutUint32(out[s+52:], 4) // align 2^4 // LC_SYMTAB. p = hdrSize + segCmdSize le.PutUint32(out[p:], machoSymtab) le.PutUint32(out[p+4:], symCmdSize) le.PutUint32(out[p+8:], uint32(symoff)) le.PutUint32(out[p+12:], uint32(len(syms))) le.PutUint32(out[p+16:], uint32(stroff)) le.PutUint32(out[p+20:], uint32(len(strtab))) // Section data. copy(out[dataOff:], code) copy(out[dataOff+len(code):], img.Data) // Relocation entries. for i, r := range relocs { e := out[reloff+i*8:] le.PutUint32(e[0:], r.addr) bits := r.symnum & 0x00ffffff bits |= 1 << 24 // r_pcrel bits |= 2 << 25 // r_length = 4 bytes if r.extern { bits |= 1 << 27 // r_extern } bits |= x8664RelocSigned << 28 le.PutUint32(e[4:], bits) } // nlist_64 entries. for i, s := range syms { e := out[symoff+i*16:] le.PutUint32(e[0:], uint32(strOff[s.name])) e[4] = s.typ e[5] = s.sect le.PutUint16(e[6:], 0) // n_desc le.PutUint64(e[8:], s.value) } // String table. copy(out[stroff:], strtab) return out, nil }