The kernel maps your segments and then stops — it does not resolve a single symbol. Before any of your code runs, someone must load the dylibs, walk the fixups from the last lesson, and run initializers. That someone is dyld, and the executable has to say where it lives: this is Mach-O's PT_INTERP, the interpreter path ELF carries in a program header. And when dyld finishes, it needs a target to jump to — Mach-O's answer to ELF's e_entry and PE's AddressOfEntryPoint.
Two load commands answer both questions. Get either wrong and nothing runs: no dylinker name means dyld is never found; no entry command means dyld has nowhere to transfer control.
Two commands, two directions of the same handshake:
/usr/lib/dyld." loader.h: "A file can have at most one of these."entryoff, "file (__TEXT) offset of main()", and stacksize, "if not zero, initial stack size for the main thread".The runtime target is arithmetic, never a stored address:
jump target = image vmaddr + entryoff + slide
vmaddr is fixed by the linker, entryoff is fixed by the linker, and only slide varies per launch — which is why MH_PIE binaries recompute this every time and why dyld, not the kernel, performs the jump. Legacy binaries pre-dating LC_MAIN used LC_UNIXTHREAD (0x5) instead, embedding the full thread-state registers (including the starting RIP) directly in the load commands.
The model's missing piece: entryoff is a file offset, and only equals a __TEXT-relative position because __TEXT.fileoff is 0 in practice — the arithmetic adds vmaddr, never fileoff.
dylinker_command = {cmd, cmdsize, name}: an lc_str offset from the command's start, then the path inside the command — the same in-command string layout LC_LOAD_DYLIB uses, so the name offset is 12 (cmd 4 + cmdsize 4 + the offset field 4), not a file offset. entry_point_command = {cmd, cmdsize 24, entryoff u64, stacksize u64}. Both commands carry LC_REQ_DYLD on LC_MAIN, so an old dyld refuses rather than skips.
The header flags of our sample decode the policy around the jump — prog64.macho: ncmds 14, sizeofcmds 968, flags 0x00200085:
| Bit | Value | loader.h says |
|---|---|---|
| MH_NOUNDEFS | 0x1 | no undefined references (all binds resolved internally or empty) |
| MH_DYLDLINK | 0x4 | input for the dynamic linker — must be loaded by dyld |
| MH_TWOLEVEL | 0x80 | two-level namespace bindings (ordinals, from Module 5) |
| MH_PIE | 0x200000 | "the OS will load the main executable at a random address" — ASLR, MH_EXECUTE only |
Both commands from prog64.macho, as otool prints them:
cmd LC_LOAD_DYLINKER
cmdsize 32
name /usr/lib/dyld (offset 12)
cmd LC_MAIN
cmdsize 24
entryoff 1104
stacksize 0Check the arithmetic: __TEXT vmaddr 0x100000000 + entryoff 1104 = 0x100000450, and nm agrees — 0000000100000450 T _main. The arm64 twin of the same program carries entryoff 1052 against the same preferred base: 0x100000000 + 1052 = 0x10000041C, and nm -arch arm64 prints 000000010000041c T _main. Different slices, different code generation, same contract — the entry command is per-arch, which is why FAT files store complete load-command sets per slice.
Note the LCs that are absent: no LC_UNIXTHREAD (LC_MAIN replaced it), and — because prog.c defines no library calls and links no dylibs — LC_LOAD_DYLINKER is the only loader-related command. The dylib sample shows the inverse: libraries never name the dylinker ("they are loaded by dyld"), so libasyncProfiler.dylib has neither command.
$ /usr/lib/llvm-21/bin/llvm-otool -arch x86_64 -l prog64.macho | grep -A3 -E "LC_LOAD_DYLINKER|LC_MAIN"
$ /usr/lib/llvm-21/bin/llvm-nm prog64.macho | grep " T _main"
0000000100000450 T _main
$ python3 -c "print(hex(0x100000000 + 1104))"
0x100000450What to look for: name offset is always 12 for LC_LOAD_DYLINKER (in-command layout, same as dylib names); stacksize 0 means the linker did not request a specific stack — the platform default applies; and python's sum lands on nm's _main exactly. Then subtract: if you ever get a number nm does not print, you added fileoff or forgot the vmaddr base.
Without looking back: what kind of value is LC_MAIN's entryoff — a file position, a load-command index, or a runtime address?
entryoff is 1104, __TEXT's vmaddr is 0x100000000, and at runtime dyld slid the image by 0xAB00000. Where does the process jump?
The pattern: entryoff is written once at link time; the jump target is recomputed every launch — vmaddr + entryoff + slide — after fixups and initializers have all succeeded.
Control transfer is the last thing dyld does with load commands before your code runs — but the commands themselves still carry the metadata macOS uses to decide whether this image may run at all: which OS floor it declares, which SDK built it, and the UUID that ties it to its dSYM.
Next: Build Versions and UUID — platform, minos, sdk, the tools that produced the file, and the 16 bytes every crash report is keyed by.