The header told you that load commands follow and how many bytes they span. The commands themselves are where the format does its actual talking: which segments to map, which symbols exist, which dylibs to load, where dyld lives, where main() is, where the code signature sits. Everything after this concept in the course — segments, sections, symbol tables, dynamic linking, entry — is one or more load commands unpacked.
They are also the format's only variable-length structure, which makes them the natural place parsers go wrong: a wrong cmdsize does not fail loudly, it shifts every subsequent command and turns a type field into garbage.
One flat array, one walk rule:
cmd (a type constant) and cmdsize (this command's total size in bytes, header and payload included).The model's missing piece: the high bit 0x80000000 (LC_REQ_DYLD) changes the failure mode. loader.h: an unknown command with the bit makes dyld refuse the image ("unknown load command required for execution"); an unknown command without it is silently ignored — forward compatibility for old tools meeting new files.
loader.h defines the universal prefix and the walk rules: struct load_command is just uint32_t cmd; uint32_t cmdsize;. The header comment adds two constraints: on 64-bit architectures cmdsize must be a multiple of 8 (4 on 32-bit), "The padded bytes must be zero," and all tables must be memory-mappable. Constants you will constantly meet — old style and LC_REQ_DYLD style:
| Constant | Value | Payload |
|---|---|---|
| LC_SEGMENT_64 | 0x19 | segment_command_64 + section_64 array |
| LC_SYMTAB / LC_DYSYMTAB | 0x2 / 0xB | symtab_command / dysymtab_command |
| LC_LOAD_DYLIB / LC_ID_DYLIB | 0xC / 0xD | dylib_command — install name, versions |
| LC_LOAD_DYLINKER | 0xE | dylinker_command — path to dyld |
| LC_UUID / LC_BUILD_VERSION | 0x1B / 0x32 | 16-byte UUID / platform, minos, sdk, tools |
| LC_CODE_SIGNATURE | 0x1D | linkedit_data_command — dataoff, datasize |
| LC_FUNCTION_STARTS / LC_DATA_IN_CODE | 0x26 / 0x29 | linkedit_data_command |
| LC_LOAD_WEAK_DYLIB / LC_RPATH | 0x80000018 / 0x8000001C | dylib_command / rpath_command (LC_REQ_DYLD set) |
| LC_DYLD_INFO_ONLY | 0x80000022 | dyld_info_command — rebase/bind/export streams |
| LC_MAIN | 0x80000028 | entry_point_command — entryoff, stacksize |
| LC_DYLD_EXPORTS_TRIE / LC_DYLD_CHAINED_FIXUPS | 0x80000033 / 0x80000034 | linkedit_data_command |
Two payloads deserve a first look now. dylib_command = cmd, cmdsize, then a dylib struct: name.offset (u32, offset from the start of the command to a null-terminated install path), timestamp, current_version, compatibility_version — versions packed as major bits 16, minor 8, patch 8 (loader.h calls the layout xxxx.yy.zz). And the ordinals: dylibs are numbered 1, 2, 3… in the order their LC_LOAD_DYLIB-family commands appear (nlist.h: "The library ordinals start from 1"); special bind values are 0 = self, −1 = main executable, −2 = flat lookup, −3 = weak lookup (loader.h BIND_SPECIAL_DYLIB_*).
The first command in prog64.macho, at byte 32:
00000020: 1900 0000 4800 0000 5f5f 5041 4745 5a45 ....H...__PAGEZE 00000030: 524f 0000 0000 0000 0000 0000 0000 0000 RO..............
19 00 00 00 = cmd 0x19 (LC_SEGMENT_64), 48 00 00 00 = cmdsize 72, then the 16-byte segname __PAGEZERO. The next command starts at 32 + 72 = 104. Walking all 14 commands of this file, with cmdsizes that sum to exactly sizeofcmds = 968:
| # | cmd | cmdsize | Meaning |
|---|---|---|---|
| 0–3 | LC_SEGMENT_64 ×4 | 72, 312, 232, 72 | __PAGEZERO, __TEXT, __DATA, __LINKEDIT |
| 4 | LC_DYLD_CHAINED_FIXUPS | 16 | dataoff 12288, datasize 56 |
| 5 | LC_DYLD_EXPORTS_TRIE | 16 | dataoff 12344, datasize 96 |
| 6–7 | LC_SYMTAB, LC_DYSYMTAB | 24, 80 | symbol table (6 syms) and dynamic symbol info |
| 8 | LC_LOAD_DYLINKER | 32 | name offset 12 → "/usr/lib/dyld" |
| 9 | LC_UUID | 24 | 4C4C44C9-5555-3144-A19F-1D5685FC622F |
| 10 | LC_BUILD_VERSION | 32 | platform 1 (macOS), minos 13.0, sdk 13.0 |
| 11 | LC_MAIN | 24 | entryoff 1104 (0x450) — file offset of main() |
| 12–13 | LC_FUNCTION_STARTS, LC_DATA_IN_CODE | 16, 16 | linkedit_data_command blobs |
That is the whole instruction sheet of an executable in 968 bytes: four segments, how to fix up pointers, where the symbols are, which linker to run first, the build identity, the entry point, and two aux tables. The signed dylib adds LC_ID_DYLIB (its own install name build/lib/libasyncProfiler.dylib at offset 24) plus two LC_LOAD_DYLIB rows — /usr/lib/libSystem.B.dylib (current version 1319.100.3) and /usr/lib/libc++.1.dylib — which become bind ordinals 1 and 2.
List the command area of any Mach-O:
$ llvm-otool -l prog64.macho | grep -E " cmd |cmdsize"
cmd LC_SEGMENT_64
cmdsize 72
cmd LC_SEGMENT_64
cmdsize 312
cmd LC_SEGMENT_64
cmdsize 232
cmd LC_SEGMENT_64
cmdsize 72
... (9 more commands, total 968 bytes)
$ xxd -s 32 -l 8 prog64.macho
00000020: 1900 0000 4800 0000 ......What to look for: the first two little-endian words are exactly cmd = 0x19 and cmdsize = 0x48. Add cmdsize to your offset repeatedly and you will land on byte 1000 = 32 + 968 — one past the last command, where the walk must stop even if more data follows in the file.
Without looking back: what two fields open every load command, and how do you find the next one?
Your parser sits at file offset 32 and reads cmdsize = 72. Where does the next command begin, and where must the walk stop in this file (ncmds 14, sizeofcmds 968)?
The pattern: offset += cmdsize until ncmds commands are read and the total consumed equals sizeofcmds. Both bounds must land together.
You can now walk the command area safely: fixed prefix at byte 32, variable lengths bounded twice over, LC_REQ_DYLD deciding what happens to unknown types. The most important command in the array is the one that maps the file into memory — LC_SEGMENT_64, four of them in every executable you have seen.
Next: LC_SEGMENT_64 — vmaddr, vmsize, fileoff, filesize, protections, and how __PAGEZERO, __TEXT, __DATA and __LINKEDIT tile both disk and address space.