Back to course

Why Mach-O Exists

15 minbeginnerModule 1: Fundamentals
15 min · Module 1: Fundamentals · Foundations

Why This Matters

Double-click an app on macOS and it runs. The file on disk — whether an app bundle's main executable, a framework, or a command-line tool — is a Mach-O file. How does the kernel know where the code starts, which libraries it must load first, where the symbols live, and which bytes are signed?

The Mach Object (Mach-O) format answers all of those questions. It is the contract between Apple's toolchain (clang, ld64) and the dynamic linker dyld, which runs before your program's main() ever executes. If you have ever wondered what otool or nm actually reads, or how code signing and ASLR work at the byte level, this format is the whole story.

Without a defined layout, no program could be loaded — every compiler would invent its own structure, and macOS could not run any of them.

A Simple Model

Think of a Mach-O file as a header followed by a list of instructions for the loader:

  1. A 32-byte header — starts with the magic CF FA ED FE (0xFEEDFACF), then the CPU type, file type, and counts.
  2. A stream of load commands — each one tells the loader something: which segments to map, which symbols exist, which dylibs to load, where the entry point is.
  3. Segment data and __LINKEDIT — the actual code and data segments, followed by the __LINKEDIT segment holding symbol tables, string tables, rebase/bind info, and code signatures.

The model's missing piece: those load commands are variable-length, so the header's ncmds and sizeofcmds fields are what let the loader walk them safely. Get those two wrong and the file will not load.

The Actual Detail

Apple's <mach-o/loader.h> defines the on-disk order exactly:

RegionStarts atSizeWhat it says
mach_header_64File offset 032 bytesmagic = 0xFEEDFACF, cputype, filetype, ncmds, sizeofcmds, flags
Load commandsOffset 32sizeofcmds bytesLC_SEGMENT_64, LC_SYMTAB, LC_LOAD_DYLIB, LC_MAIN, ...
Segment dataPer-segment fileoffPer-segment filesize__TEXT, __DATA_CONST, __DATA, __LINKEDIT contents
__LINKEDITLast segment's fileofffilesizeSymbols, strings, rebase/bind opcodes, export trie, code signature

Unlike PE (which hides the real header behind an MS-DOS stub) or ELF (which has two header tables), Mach-O puts its single header at byte 0 and follows it with one flat array of load commands. The vocabulary you will use all course: file offset is a position on disk; vmaddr is where a segment lands in memory; __LINKEDIT is the trailing metadata segment that dyld and the kernel both read.

Common mistake. You might think Mach-O always starts with feedface as a single big-endian word. It does not — the bytes on disk are CF FA ED FE, which is 0xFEEDFACF read little-endian. FAT (universal) binaries are the exception: they start CA FE BA BE (0xCAFEBABE big-endian).

A Real Example

First 32 bytes of a real 64-bit Mach-O object file (prog64.o, built with clang -target x86_64-apple-darwin -c):

00000000: cffaedfe 07000001 03000000 01000000  ................
00000010: 04000000 00020000 00200000 00000000  ......... ......

Walk-through:

  1. Bytes 0-3: CF FA ED FE — MH_MAGIC_64 (0xFEEDFACF little-endian), so this is a 64-bit Mach-O.
  2. Bytes 4-7: 07 00 00 01 — cputype 0x01000007 = CPU_TYPE_X86_64.
  3. Bytes 8-11: 03 00 00 00 — cpusubtype 3 = CPU_SUBTYPE_X86_64_ALL.
  4. Bytes 12-15: 01 00 00 00 — filetype 1 = MH_OBJECT (a relocatable object, not an executable).
  5. Bytes 16-19: 04 00 00 00 — ncmds = 4 load commands follow.
  6. Bytes 20-23: 00 02 00 00 — sizeofcmds = 0x200 = 512 bytes.
  7. Bytes 24-27: 00 20 00 00 — flags = 0x2000 = MH_SUBSECTIONS_VIA_SYMBOLS.

That is a complete header decode from 32 bytes — the pattern every Mach-O parse starts with.

Try It

Any Mach-O file works — app binaries, dylibs, or object files. On Linux or macOS:

$ file /usr/bin/ls
/usr/bin/ls: Mach-O 64-bit executable arm64

$ xxd -l 32 /usr/bin/ls
00000000: cffaedfe 0c000001 00000000 02000000  ................
...

What to look for: the first four bytes are always CF FA ED FE for thin 64-bit files, and every multi-byte field you read from the file is little-endian — the lowest-address byte is the least significant. That single rule decodes half the format (FAT headers are the big-endian exception).

Check Your Understanding

Without looking back: what four bytes open a thin 64-bit Mach-O file, and what 32-bit value do they form little-endian?

Apply It

A Mach-O header has ncmds = 14 and sizeofcmds = 968. The header is 32 bytes. At what file offset does the first load command start, and where does segment data begin?

The pattern: header is always 32 bytes, load commands start at 32, and the first segment's fileoff is normally 32 + sizeofcmds (though segments can declare any fileoff).

Connect

This course follows the same spine the ELF and PE courses did: headers first (what the loader reads), then addressing (how disk maps to memory), then segments and the tables inside them (symbols, dynamic linking, exports), and finally the loader (dyld) itself.

Next: Mach-O File Layout — the full map from byte 0 to the last byte of __LINKEDIT, before any individual field is decoded.