The compiler emits prog64.o without knowing where _helper will finally live, so the call instruction ships as e8 00 00 00 00 — four placeholder zeros. Something must tell the linker: "at offset 0x12 in __text there is a 4-byte PC-relative branch against symbol _helper." That something is the relocation entry — Mach-O's equivalent of ELF's .rela.text, and the reason object files and linked images are different species.
This is the last concept where the linker, not dyld, is the audience. Once you see that final executables carry zero classic relocations, the design of rebase/bind opcodes — and why images need them at all — stops being a mystery.
Each section_64 may point at an array (reloff, nreloc), one 8-byte record per patch site:
How the fields combine: r_extern = 1 means r_symbolnum is an index into the symbol table — patch against _helper, entry 0. r_extern = 0 means it is a section ordinal — patch against another section (this is how 8-byte pointers to __text in unwind data get resolved, with r_type UNSIGNED). r_length is log2 of the slot width: 0 = 1 byte, 1 = 2 bytes, 2 = 4 bytes, 3 = 8 bytes. r_pcrel says the patched value is relative to the next instruction, not absolute.
The model's missing piece: r_type is architecture vocabulary. The same number 2 means BRANCH on x86_64 and BRANCH26 on arm64 — you cannot decode a relocation without first knowing the file's cputype (there is also a scattered_relocation_info form for 32-bit files; 64-bit code does not use it).
Within the packed u32, bit positions are: symbolnum 0-23, pcrel bit 24, length bits 25-26, extern bit 27, type bits 28-31. The type enums, from Apple's reloc headers (as mirrored in LLVM's BinaryFormat/MachO.h):
| x86_64 type | Name | arm64 type | Name |
|---|---|---|---|
| 0 | X86_64_RELOC_UNSIGNED | 0 | ARM64_RELOC_UNSIGNED |
| 1 | X86_64_RELOC_SIGNED | 1 | ARM64_RELOC_SUBTRACTOR |
| 2 | X86_64_RELOC_BRANCH | 2 | ARM64_RELOC_BRANCH26 |
| 3 | X86_64_RELOC_GOT_LOAD | 3 | ARM64_RELOC_PAGE21 |
| 4 | X86_64_RELOC_GOT | 4 | ARM64_RELOC_PAGEOFF12 |
| 5 | X86_64_RELOC_SUBTRACTOR | 5-6 | GOT_LOAD_PAGE21 / PAGEOFF12 |
| 6-9 | SIGNED_1 / SIGNED_2 / SIGNED_4 / TLV | 7-11 | POINTER_TO_GOT, TLVP_LOAD_*, ADDEND, AUTHENTICATED_POINTER |
Semantics you will meet constantly: SIGNED patches a RIP-relative displacement (the linker adds the delta); BRANCH patches a call/jump displacement; PAGE21 + PAGEOFF12 are arm64's two-instruction ADRP+ADD/LOAD pair; UNSIGNED writes a raw 64-bit address (pointers, with extern = 0 and a section target).
All five relocations in prog64.o's __text (reloff 816, nreloc 5), decoded — every one is pcrel = 1, length = 2 (4-byte slot), extern = 1:
| r_address | r_type | Target (symnum) | Instruction it patches |
|---|---|---|---|
| 0x12 | 2 BRANCH | 0 = _helper | call disp32 — placeholder e8 00 00 00 00 at 0x11 |
| 0x1B | 1 SIGNED | 3 = _global_counter | RIP-relative addl reading __data |
| 0x5A | 2 BRANCH | 1 = _add | call disp32 from _main to _add |
| 0x60 | 1 SIGNED | 2 = _bss_counter | RIP-relative store into __bss slot |
| 0x66 | 1 SIGNED | 2 = _bss_counter | RIP-relative load from that slot |
Notice r_address points at the displacement, not the opcode — 0x12 sits one byte after the 0xE8 at 0x11. The section's own bytes show the zeros waiting to be filled: __text starts 55 48 89 e5 48 83 ec 10, and the linker's only job for these entries is computing target-minus-site and writing it into each 4-byte hole.
The arm64 object makes the extern = 0 case visible: prog_arm64.o's __compact_unwind holds three UNSIGNED relocations, length = 3 (8-byte slot), extern = 0, symbolnum = 1 — "write the address of section 1 (__text) here," the unwind records pointing at their function's section.
$ llvm-readobj --relocations prog64.o
Relocations [
Section __text {
0x66 1 2 1 X86_64_RELOC_SIGNED 0 _bss_counter
0x60 1 2 1 X86_64_RELOC_SIGNED 0 _bss_counter
0x5A 1 2 1 X86_64_RELOC_BRANCH 0 _add
0x1B 1 2 1 X86_64_RELOC_SIGNED 0 _global_counter
0x12 1 2 1 X86_64_RELOC_BRANCH 0 _helper
}
]The columns are address, pcrel, length, extern, type name, addend, symbol — matching the packed word you decoded by hand. Swap in prog_arm64.o to watch the type names change to ARM64_RELOC_PAGE21, PAGEOFF12, and BRANCH26 while the bit layout stays identical.
Without looking back: a relocation entry reports r_length = 2. What width does it patch?
You check prog64.macho (the linked executable) and find __text has reloff = 0 and nreloc = 0, while prog64.o has five. Is the executable broken?
The pattern: reloff/nreloc non-zero means "still being linked." The moment those fields go to zero, symbol binding has moved to the dyld-side tables — which the next two concepts begin to build.
Relocations reference the same nlist_64 array indexed by The Dynamic Symbol Table — r_extern = 1 means "look up this symtab index," which is why the two concepts sit side by side.
Next: Dynamic Libraries and Install Names — extern references only resolve if dyld knows which libraries to load and what to call them at runtime: LC_LOAD_DYLIB, install names, and the ordinals bind opcodes will use.