Back to course

Relocation Entries

18 minintermediateModule 4: SymbolsContinues from macho-dysymtab
18 min · Module 4: Symbols

Why This Matters

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.

A Simple Model

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).

The Actual Detail

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 typeNamearm64 typeName
0X86_64_RELOC_UNSIGNED0ARM64_RELOC_UNSIGNED
1X86_64_RELOC_SIGNED1ARM64_RELOC_SUBTRACTOR
2X86_64_RELOC_BRANCH2ARM64_RELOC_BRANCH26
3X86_64_RELOC_GOT_LOAD3ARM64_RELOC_PAGE21
4X86_64_RELOC_GOT4ARM64_RELOC_PAGEOFF12
5X86_64_RELOC_SUBTRACTOR5-6GOT_LOAD_PAGE21 / PAGEOFF12
6-9SIGNED_1 / SIGNED_2 / SIGNED_4 / TLV7-11POINTER_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).

Common mistake. Looking for relocations in a linked program. MH_OBJECT files carry them for the linker; MH_EXECUTE and MH_DYLIB images do not — loader.h says executable and object relocation entries "continue to hang off the section structures," while dylib pools sit in LC_DYSYMTAB (and modern 64-bit dylibs leave those pools empty too). Images fix addresses at load time with rebase/bind info or chained fixups instead. prog64.macho's __text has nreloc = 0, correctly.

A Real Example

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_addressr_typeTarget (symnum)Instruction it patches
0x122 BRANCH0 = _helpercall disp32 — placeholder e8 00 00 00 00 at 0x11
0x1B1 SIGNED3 = _global_counterRIP-relative addl reading __data
0x5A2 BRANCH1 = _addcall disp32 from _main to _add
0x601 SIGNED2 = _bss_counterRIP-relative store into __bss slot
0x661 SIGNED2 = _bss_counterRIP-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.

Try It

$ 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.

Check Your Understanding

Without looking back: a relocation entry reports r_length = 2. What width does it patch?

Apply It

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.

Connect

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.