Back to course

LC_SEGMENT_64

18 minintermediateModule 3: Load CommandsContinues from macho-load-commands
18 min · Module 3: Load Commands

Why This Matters

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.

A Simple Model

One command per contiguous region, answering four questions:

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.

The Actual Detail

loader.h defines segment_command_64 as exactly 72 bytes before any section records:

OffsetSizeFieldWhat it says
04cmdAlways 0x19 (LC_SEGMENT_64)
44cmdsizeThis struct plus nsects × 80-byte section records
816segnameNUL-padded name such as "__TEXT"
248vmaddrMemory address the segment loads at
328vmsizeMemory size; may exceed filesize (zero fill)
408fileoffFile offset of the segment contents
488filesizeBytes mapped from the file
564maxprotMaximum protection (vm_prot_t)
604initprotInitial protection actually applied
644nsectsNumber of section_64 records that follow
684flagsSG_* 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.

Common mistake. Treating cmdsize as "just the struct." loader.h says cmdsize "includes sizeof section_64 structs" — a __TEXT command with 3 sections is 72 + 3×80 = 312 bytes. Second mistake: expecting every file to have named segments. MH_OBJECT files pack all sections into one anonymous segment; prog64.o's single command has an empty segname and prot 7/7 (rwx) because the linker has not assigned final permissions yet.

A Real Example

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                      ........
  1. 19 00 00 00 = cmd 0x19, 48 00 00 00 = cmdsize 72 — no sections follow.
  2. Bytes 8-23: __PAGEZERO padded with NULs to 16.
  3. vmsize at offset 32 reads 00 00 00 00 01 00 00 00 little-endian = 0x100000000 = 4 GiB of guarded address space.
  4. fileoff, filesize, maxprot, initprot, nsects, flags are all zero — 4 GiB of memory, zero bytes of file, no permissions. The guard page, stated in binary.

The executable's four segments, decoded from the same file:

segnamevmaddr / vmsizefileoff / filesizeinitprotnsects
__PAGEZERO0 / 0x1000000000 / 000
__TEXT0x100000000 / 0x20000 / 81925 (r-x)3
__DATA0x100002000 / 0x10008192 / 40963 (rw-)2
__LINKEDIT0x100003000 / 0x14812288 / 3281 (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.

Try It

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.

Check Your Understanding

Without looking back: otool prints initprot 0x00000005 for __TEXT. Which access does that grant?

Apply It

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.

Connect

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.