Segments decide where memory comes from; sections decide what it means. ELF programmers reach for .text, .data, .bss; PE programmers for .text and .rdata. Mach-O's equivalents are sections — __text, __data, __cstring, __stubs — and they carry the metadata that lets every tool behave correctly: the linker merges same-named sections, nm finds functions, dyld discovers stub tables and initialiser arrays, debuggers skip sections marked S_ATTR_DEBUG.
The flags field is the contract. Get it wrong and tools will execute your data or treat code as strings. One 32-bit word separates a section of machine instructions from a section of NUL-terminated C strings.
Immediately after each segment_command_64 come nsects fixed-size records — section_64, 80 bytes each:
sectname[16] ("__text") and segname[16] ("__TEXT"). Every section repeats its parent segment's name; the pair (__TEXT, __text) uniquely identifies it, which is exactly how otool prints sections.addr and size, in memory (addr sits inside the segment's vmaddr range).offset plus align, the alignment as a log2 power (4 means 2^4 = 16 bytes).reloff and nreloc, used by object files (covered two concepts from now).flags, split into a type (low 8 bits) and any number of attributes (upper 24 bits), plus three reserved words.The model's missing piece: sections are numbered. loader.h and nlist.h number them 1, 2, 3... in load-command order across all segments of the file — that number is what a symbol's n_sect field stores, so "section 4" means "the fourth section_64 record the loader walked past."
loader.h lays out section_64 as 80 bytes: sectname[16], segname[16], addr u64, size u64, offset u32, align u32, reloff u32, nreloc u32, flags u32, reserved1 u32, reserved2 u32, reserved3 u32. It then defines the flags split: SECTION_TYPE = 0x000000ff, SECTION_ATTRIBUTES = 0xffffff00. The types you will actually meet:
| Type (low 8) | Name | Contains |
|---|---|---|
| 0x0 | S_REGULAR | Ordinary bytes |
| 0x1 | S_ZEROFILL | Uninitialised space — no file bytes, zero-filled at load |
| 0x2 | S_CSTRING_LITERALS | NUL-terminated C strings |
| 0x6 | S_NON_LAZY_SYMBOL_POINTERS | GOT-style pointers, bound before main |
| 0x7 | S_LAZY_SYMBOL_POINTERS | Lazy binding slots (la_symbol_ptr) |
| 0x8 | S_SYMBOL_STUBS | Jump stubs that trigger lazy binding |
| 0x9 | S_MOD_INIT_FUNC_POINTERS | Initialiser function pointers (constructors) |
| 0xb | S_COALESCED | Symbols to coalesce across objects (eh_frame) |
The attribute bits you will meet in real files: S_ATTR_PURE_INSTRUCTIONS = 0x80000000, S_ATTR_SOME_INSTRUCTIONS = 0x00000400, S_ATTR_DEBUG = 0x02000000, S_ATTR_EXT_RELOC = 0x00000200, S_ATTR_LOC_RELOC = 0x00000100. Two reserved words are not reserved in practice: for stub and symbol-pointer sections, reserved1 is the section's start index into the indirect symbol table and reserved2 is the stub's byte size — loader.h documents both.
All four sections of prog64.o, straight from otool:
sectname __text
offset 544
align 2^4 (16)
reloff 816
nreloc 5
flags 0x80000400
sectname __data
offset 664
align 2^2 (4)
reloff 0
nreloc 0
flags 0x00000000
sectname __common
offset 0
align 2^2 (4)
reloff 0
nreloc 0
flags 0x00000001
sectname __eh_frame
offset 672
align 2^3 (8)
reloff 0
nreloc 0
flags 0x6800000bA linked dylib shows the richer cases: __stubs has flags 0x80000408 (type 8, instructions) with reserved2 = 6 (each stub is 6 bytes); __cstring has flags 2; __la_symbol_ptr flags 7; __got flags 6 with reserved1 = 172 — the index where its 41 indirect-symbol entries begin.
$ otool -l prog64.o | grep -E "^ sectname|^ offset|^ align|^ reloff|^ nreloc|^ flags"
sectname __text
offset 544
align 2^4 (16)
reloff 816
nreloc 5
flags 0x80000400
...What to look for: align 2^n is a power-of-two exponent, not a byte count (2^4 = 16); a non-zero reloff/nreloc pair marks an unlinked object file; and every section you see is followed by loader.h's rule that same-named sections in the same segment get merged by the link editor into one aligned, zero-padded result.
Without looking back: prog64.o's __text section reports flags = 0x80000400. What does that say?
libasyncProfiler.dylib's __bss section prints size 0x9739, offset 0, align 2^4, flags 0x00000001. Where do its bytes live on disk?
The pattern: offset only means something when the section type stores file bytes. Check the type first, then the offset — never the reverse.
Sections subdivide the LC_SEGMENT_64 commands you just decoded — same command stream, one level finer, with the flags word doing the heavy lifting.
Next: The Symbol Table — every symbol's n_sect field is one of these section numbers, and the names you have been reading as strings live in a separate table in __LINKEDIT.