Back to course

The Load Command Area

20 minintermediateModule 3: Load CommandsContinues from macho-universal
20 min · Module 3: Load Commands

Why This Matters

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.

A Simple Model

One flat array, one walk rule:

  1. Start at byte 32, right after mach_header_64. Read two u32s: cmd (a type constant) and cmdsize (this command's total size in bytes, header and payload included).
  2. Advance by cmdsize — add it to the current offset to reach the next command. Repeat until you have read ncmds commands or consumed sizeofcmds bytes; never trust one without checking the other.
  3. Dispatch on cmd — each type has its own payload struct (segment_command_64, dylib_command, entry_point_command…), but all of them begin with the same cmd + cmdsize prefix.

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.

The Actual Detail

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:

ConstantValuePayload
LC_SEGMENT_640x19segment_command_64 + section_64 array
LC_SYMTAB / LC_DYSYMTAB0x2 / 0xBsymtab_command / dysymtab_command
LC_LOAD_DYLIB / LC_ID_DYLIB0xC / 0xDdylib_command — install name, versions
LC_LOAD_DYLINKER0xEdylinker_command — path to dyld
LC_UUID / LC_BUILD_VERSION0x1B / 0x3216-byte UUID / platform, minos, sdk, tools
LC_CODE_SIGNATURE0x1Dlinkedit_data_command — dataoff, datasize
LC_FUNCTION_STARTS / LC_DATA_IN_CODE0x26 / 0x29linkedit_data_command
LC_LOAD_WEAK_DYLIB / LC_RPATH0x80000018 / 0x8000001Cdylib_command / rpath_command (LC_REQ_DYLD set)
LC_DYLD_INFO_ONLY0x80000022dyld_info_command — rebase/bind/export streams
LC_MAIN0x80000028entry_point_command — entryoff, stacksize
LC_DYLD_EXPORTS_TRIE / LC_DYLD_CHAINED_FIXUPS0x80000033 / 0x80000034linkedit_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_*).

Common mistake. cmdsize is not "the payload size" — it includes the 8-byte prefix plus payload plus padding. Also, ncmds and sizeofcmds must agree: if your walk ends early or runs past 32 + sizeofcmds, one of the two header counts (or a cmdsize) is wrong, and you must stop rather than read on.

A Real Example

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:

#cmdcmdsizeMeaning
0–3LC_SEGMENT_64 ×472, 312, 232, 72__PAGEZERO, __TEXT, __DATA, __LINKEDIT
4LC_DYLD_CHAINED_FIXUPS16dataoff 12288, datasize 56
5LC_DYLD_EXPORTS_TRIE16dataoff 12344, datasize 96
6–7LC_SYMTAB, LC_DYSYMTAB24, 80symbol table (6 syms) and dynamic symbol info
8LC_LOAD_DYLINKER32name offset 12 → "/usr/lib/dyld"
9LC_UUID244C4C44C9-5555-3144-A19F-1D5685FC622F
10LC_BUILD_VERSION32platform 1 (macOS), minos 13.0, sdk 13.0
11LC_MAIN24entryoff 1104 (0x450) — file offset of main()
12–13LC_FUNCTION_STARTS, LC_DATA_IN_CODE16, 16linkedit_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.

Try It

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.

Check Your Understanding

Without looking back: what two fields open every load command, and how do you find the next one?

Apply It

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.

Connect

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.