ELF spreads mapping decisions across program headers; PE hides them behind its section table. Mach-O puts every mapping decision in one explicit command: LC_SEGMENT_64 tells the kernel and dyld exactly which run of file bytes to map, where it lands in memory, and with which read/write/execute permissions. Nothing in a Mach-O file becomes memory without one of these commands.
This is also where the famous names live — __TEXT, __DATA, __LINKEDIT, __PAGEZERO. When someone says "the __TEXT segment is read-execute," they are quoting fields in this structure. Sections, symbols, and fixups all hang off segments declared here.
One command per contiguous region, answering four questions:
fileoff and filesize.vmaddr and vmsize. The file supplies the first filesize bytes; if vmsize is larger, the tail is zero-filled. That is how uninitialised space exists without costing disk.maxprot (the ceiling) and initprot (what the mapping actually starts with): read = 1, write = 2, execute = 4.nsects section records follow the struct (next lesson).The model's missing piece: the conventional segments have jobs. __PAGEZERO maps no file bytes (filesize 0) and holds the lowest 4 GiB with no permissions, so NULL dereferences fault — loader.h says it "catches NULL references for MH_EXECUTE files". __TEXT carries code, __DATA carries mutable data, and __LINKEDIT is the trailing metadata pile dyld reads.
loader.h defines segment_command_64 as exactly 72 bytes before any section records:
| Offset | Size | Field | What it says |
|---|---|---|---|
| 0 | 4 | cmd | Always 0x19 (LC_SEGMENT_64) |
| 4 | 4 | cmdsize | This struct plus nsects × 80-byte section records |
| 8 | 16 | segname | NUL-padded name such as "__TEXT" |
| 24 | 8 | vmaddr | Memory address the segment loads at |
| 32 | 8 | vmsize | Memory size; may exceed filesize (zero fill) |
| 40 | 8 | fileoff | File offset of the segment contents |
| 48 | 8 | filesize | Bytes mapped from the file |
| 56 | 4 | maxprot | Maximum protection (vm_prot_t) |
| 60 | 4 | initprot | Initial protection actually applied |
| 64 | 4 | nsects | Number of section_64 records that follow |
| 68 | 4 | flags | SG_* bits (for example SG_READ_ONLY = 0x10) |
The protection bits come from Apple's vm_prot.h: VM_PROT_READ = 1, VM_PROT_WRITE = 2, VM_PROT_EXECUTE = 4. So 5 is read+execute, 3 is read+write, 7 is everything, 0 is a trap page.
The first load command of prog64.macho, all 72 bytes starting at file offset 32:
00000020: 1900 0000 4800 0000 5f5f 5041 4745 5a45 ....H...__PAGEZE 00000030: 524f 0000 0000 0000 0000 0000 0000 0000 RO.............. 00000040: 0000 0000 0100 0000 0000 0000 0000 0000 ................ 00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000060: 0000 0000 0000 0000 ........
19 00 00 00 = cmd 0x19, 48 00 00 00 = cmdsize 72 — no sections follow.__PAGEZERO padded with NULs to 16.00 00 00 00 01 00 00 00 little-endian = 0x100000000 = 4 GiB of guarded address space.The executable's four segments, decoded from the same file:
| segname | vmaddr / vmsize | fileoff / filesize | initprot | nsects |
|---|---|---|---|---|
| __PAGEZERO | 0 / 0x100000000 | 0 / 0 | 0 | 0 |
| __TEXT | 0x100000000 / 0x2000 | 0 / 8192 | 5 (r-x) | 3 |
| __DATA | 0x100002000 / 0x1000 | 8192 / 4096 | 3 (rw-) | 2 |
| __LINKEDIT | 0x100003000 / 0x148 | 12288 / 328 | 1 (r--) | 0 |
The segments tile memory end to end (0x100000000, then +0x2000, then +0x1000), and __TEXT covers the header and load commands itself — fileoff 0, because they are mapped with the code.
Pull just the segment fields out of the load-command dump (samples directory):
$ otool -arch x86_64 -l prog64.macho | grep -E "^ segname|^ vmaddr|^ vmsize|^ fileoff|^ filesize|^ initprot|^ nsects"
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
initprot 0x00000000
nsects 0
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000002000
fileoff 0
filesize 8192
initprot 0x00000005
nsects 3
...What to look for: initprot values only ever combine bits 1, 2, and 4; filesize never exceeds vmsize; and __LINKEDIT is last with nsects 0 — link-edit metadata is one blob, not sections.
Without looking back: otool prints initprot 0x00000005 for __TEXT. Which access does that grant?
A segment_command_64 reports nsects = 3. What is its cmdsize, and why?
The pattern: cmdsize is the whole command's footprint. Add it to the current offset to reach the next load command — that is the walk the loader performs ncmds times.
You now know how a segment claims memory. The previous concept, The Load Command Area, showed how the header points at this command stream; the segment command is simply its most important member.
Next: Sections — the nsects records packed behind this command, where names, alignment flags, and the S_* type bits live.