main() feels like the first instruction your program executes. It is not. Between your shell and main sit the kernel, dyld, and — in a normal binary — a startup stub that builds the argument vector before anyone asked for it. Miss that sequence and whole bug classes are unexplainable: crashes before main, constructors firing in "random" order, argv empty in a debugger that broke at entry, an exit code that does not match your return statement.
It is also the payoff for the whole course. LC_MAIN is one load command; reading it correctly requires the header, the segments, the slide, and the entry-point arithmetic you have all met. One number, four concepts deep.
The sequence, from the outside in:
/usr/lib/dyld, and hands over control.loader.h says of the thread-command tradition that "command arguments and environment variables are copied onto that stack" — and the platform's C runtime initializes before calling main.The model's missing piece — the classic mistake — is stage 4's target. loader.h comments LC_MAIN's entryoff as "file (__TEXT) offset of main()", and our sample takes that comment literally: the offset lands on _main itself, startup stub skipped, because this tiny link contains no CRT. An ordinary clang-linked binary points entryoff at the startup stub in libsystem instead; only after that stub packs argc/argv/envp/apple does main() finally run. Same load command, two linker choices.
The command itself — entry_point_command, "a replacement for thread_command" (the old LC_UNIXTHREAD, which carried the initial stack). Its whole body for prog64.macho:
cmd LC_MAIN cmdsize 24 entryoff 1104 stacksize 0
cmdsize 24, entryoff 1104 (0x450), stacksize 0 meaning "use the default" (the comment: "if not zero, initial stack size"). Now the three-number identity that proves what this value is:
| Fact | Value | Source |
|---|---|---|
| LC_MAIN.entryoff | 1104 (0x450) | load command 11 of prog64.macho |
| __TEXT vmaddr / fileoff | 0x100000000 / 0 | LC_SEGMENT_64 — fileoff 0 means offset maps 1:1 onto the base |
| Computed preferred entry | 0x100000000 + 1104 = 0x100000450 | arithmetic |
| nm: _main | 0000000100000450 T _main | the symbol table — exact match |
Three independent readings agree to the byte: this binary's entry is main. Notice also where 1104 sits — past the load commands (which end at byte 999), inside __text (file 1040–1156): entryoff addresses code, never metadata. And 0x100000450 is already past __mh_execute_header at 0x100000000 — the image-base marker symbol, Mach-O's sibling of PE's ImageBase.
Follow one instruction pointer from file bytes to first opcode. The 24 bytes at file offset 0x450 — entryoff itself as a file position:
00000450: 5548 89e5 4883 ec10 c745 fc00 0000 00bf UH..H....E...... 00000460: 0100 0000 be02 0000 ........
55 = push %rbp, 48 89 E5 = mov %rsp,%rbp, 48 83 EC 10 = sub $0x10,%rsp — the standard function prologue, main's own, not a startup stub's. There is no CRT shim between dyld's jump and this push, because this link has no CRT to put there. Disassembled with addresses:
100000450: 55 pushq %rbp 100000451: 48 89 e5 movq %rsp, %rbp 100000454: 48 83 ec 10 subq $0x10, %rsp 100000458: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp) 10000045f: bf 01 00 00 00 movl $0x1, %edi
The last two lines are our source's main getting ready to call add(1, 2) — the program text you read in prog.c, at the address LC_MAIN predicted. Runtime corollary: with a slide of 0x4000 every one of these bytes lives at 0x100004450 instead; the file never changes, the jump target does.
Prove the identity on any executable — on macOS the linker tools are otool, nm, objdump; LLVM ships llvm- prefixed versions:
$ python3 -c "print(hex(0x100000000 + 1104))"
0x100000450
$ llvm-nm prog64.macho | grep -E "_main|__mh"
0000000100000000 T __mh_execute_header
0000000100000450 T _main
$ llvm-objdump -d prog64.macho | grep -A4 "^100000450"
100000450: 55 pushq %rbp
100000451: 48 89 e5 movq %rsp, %rbp
100000454: 48 83 ec 10 subq $0x10, %rsp
100000458: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp)
10000045f: bf 01 00 00 00 movl $0x1, %ediWhat to look for: python's sum lands on nm's _main address exactly, and the disassembly starts with a function prologue — entry is a callable function, not a table or a trampoline. On your own binaries, read entryoff from otool first: if it lands on a symbol named start rather than main, you are looking at the CRT-era behavior this lesson described.
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 0x4000. At what address does the process jump?
The pattern: entryoff is written once, at link time; the jump target is recomputed every launch — vmaddr + entryoff + slide — and everything before it must succeed first.
That is the full arc: 32 header bytes that choose the parser, load commands that map, protect, and fix; a signature and UUID that attest; a dSYM that remembers; dyld that performs; a layout that slides; and one offset that fires. The bytes are still on disk exactly as you first read them — every lesson in this course was a different projection of the same file.
Back to the course: all 24 concepts, the samples to practice on, and the spec sections each claim came from.