A Hole and a Record established that a relocatable file is a hole plus a record, and that the record answers four questions. It showed you one record. It did not show you the record, because the four questions have three completely different physical answers and picking the wrong one is how a reader silently produces plausible nonsense.
THE FOUR QUESTIONS, and where each format keeps the answer
ELF64 one 24-byte record, three fields
r_offset u64 WHERE
r_info u64 WHAT + WHOM, packed together
r_addend i64 HOW MUCH (signed!)
COFF x64 one 10-byte record, three fields
VirtualAddress u32 WHERE
SymbolTableIndex u32 WHOM
Type u16 WHAT
Mach-O one 8-byte record, two fields
r_address i32 WHERE (signed)
r_info u32 six things, bit-packed
WHOM(24) PCREL(1) LENGTH(2)
EXTERN(1) TYPE(4) = 32 bits exactly
Three designs, one job. And the differences are not cosmetic. A reader that assumes the ELF layout will read a COFF file's symbol index as a relocation type and get a number that is a perfectly valid relocation — just the wrong one. That is the failure this concept is about, and it is why the record has to be read from the specification rather than extrapolated from the format you know best.
The frame throughout is the same one the course has used since the second concept: every field exists because the linker needs it for something specific. There are no alignment fields here, no reserved fields, no versioning. Every byte is load-bearing, and the interesting part is that the three formats load different things into it.
Before the layouts, the abstraction, because the abstraction is what makes the three layouts comparable. Strip away the encoding and a relocation record is four facts:
Now the design pressure, which is what actually explains the differences. A record must be findable from the section it patches, and the section must be findable from the record. Every format solves that linkage differently, and the solution determines the layout:
SHT_RELA section is a sibling of the data sections, and the association lives in the relocation section's own header: sh_link is the symbol table, sh_info is the section being patched. So the offset field only has to be a section-relative offset — it does not repeat the section, because the header already said which section.PointerToRelocations in the section header. So the association is positional: everything between that pointer and the count is this section's relocations. The offset field only has to be an offset, for the same reason ELF's does.reloff and nreloc, exactly as COFF does. But its offset field is a section address, not a section offset — because a Mach-O object gives its sections provisional addresses. This is the one case where the same small field means a genuinely different thing, and it is the sharpest cross-format trap in the course.whom — ELF buries it in the high half of r_info, COFF gives it a field of its own, and Mach-O squeezes it into 24 bits alongside five other facts. The model is about the information; the layouts are three answers to the question of how to lay it out.The first record of .rela.text in demo_elf.o, which is the call to global_counter at text offset 4. All 24 bytes, from the file at 0x240:
00000240: 0400 0000 0000 0000 0200 0000 0500 0000 ................
00000250: fcff ffff ffff ffff ................
+0 r_offset u64 0x0000000000000004 text offset 4
+8 r_info u64 0x0000000500000002
low 32 bits = 0x2 = R_X86_64_PC32 WHAT
high 32 bits = 0x5 = symbol index 5 WHOM
+16 r_addend i64 0xfffffffffffffffc = -4 HOW MUCH
Three things in those 24 bytes are worth pulling apart, because two of them are the opposite of what the field names suggest.
r_info is one 64-bit number holding a symbol index and a type. The type is in the LOW half and the symbol in the HIGH half — so the packing reads r_info = (sym << 32) | type, and the mnemonic order is the reverse of the arithmetic. Confirm it against readelf, which prints the whole 64-bit value in one column: 0000000500000002. The 05 at the left of that number is the symbol; the 02 at the right is the type.
The 32-bit form uses a different split, which is a real trap: Elf32_Rel packs as (sym << 8) | type — the same order, but 8 bits of type instead of 32, in a 32-bit word. Here is a real record from demo_i386_nopic.o's .rel.text:
08 00 00 00 01 03 00 00
+0 r_offset u32 = 0x00000008
+4 r_info u32 = 0x00000301
low BYTE = 0x01 = R_386_32 WHAT
high WORD = 0x0003 = symbol 3 WHOM
And note what is absent: there is no addend field. That is the REL versus RELA distinction, and it is the subject of the next concept because the addend's location changes what the section bytes must contain. The record here is 8 bytes, not 12 or 24.
r_addend is an i64, and here it is 0xfffffffffffffffc. Read as unsigned that is 18,446,744,073,709,551,804; read as signed it is -4. A reader that treats it as unsigned gets a plausible enormous number and a link error several steps later.
Why -4: the call at text offset 4 is a five-byte e8 rel32 whose displacement field is at offset 5, and x86-64 measures that displacement from the end of the instruction, not from the displacement field itself. The linker computes S - P where P is the field's address; the field is four bytes before the instruction's end, so the stored constant must be -4 to make the arithmetic come out right. The four bytes in .text at offset 4 are all zero — the whole correction lives in the record. The next concept takes that apart properly.
All four .text relocations of demo_coff.o, at 0x16a, 10 bytes each:
0000016a: 0400 0000 1100 0000 0400 ....... .... 00000174: 0a00 0000 1200 0000 0400 ....... .... 0000017e: 2100 0000 1400 0000 0400 !.......... 00000188: 3300 0000 1600 0000 0400 3..........
Ten bytes, and all three fields are unpacked. No bit twiddling, no shared word, nothing to mask. The cost is size: ELF spends 24 bytes on the same information, and Mach-O spends 8. The benefit is that a COFF reader never has to know how a type number and a symbol index share a word, which is the mistake a reader makes when it assumes ELF's packing.
And all four are REL32 — including the call at offset 0x21. There is no PLT32 on this target. Compare the relocation table concept's finding that ELF distinguishes R_X86_64_PC32 from R_X86_64_PLT32 purely by a hint. COFF-x86-64 makes a call and a data reference the same relocation, so on this target a linker cannot tell them apart from the record and must decide from context. That is not a simplification; it is the whole design, and it is why a linker implements a vocabulary per target rather than per format.
All four __text relocations of demo_macho.o at 0x3b8, 8 bytes each, in the order they appear in the file:
000003b8: 3700 0000 0300 001d 7....... addr 0x37 000003c0: 2600 0000 0700 002d &......- addr 0x26 000003c8: 0e00 0000 0500 001d ....... addr 0x0e 000003d0: 0800 0000 0200 001d ....... addr 0x08
Take the first: r_address = 0x37, r_info = 0x1d000003. Decode the word as five bitfields packed into 32 bits, low field first:
Verify the arithmetic: 1<<28 | 1<<27 | 2<<25 | 1<<24 | 3 is 0x10000000 + 0x08000000 + 0x04000000 + 0x01000000 + 3 = 0x1d000003. Exactly the bytes in the file. And llvm-readobj-21 prints the same record as 0x37 1 2 1 X86_64_RELOC_SIGNED 0 _message — which is those five fields in a different order, plus the field width, for free.
Three consequences fall out of that layout, and all three are load-bearing for an implementation.
r_length = 2 means 4 bytes, and the encoding is logarithmic: 1 << r_length. ELF and COFF instead make the width a property of the type, so the width is implied by which relocation you have. Mach-O is the only one of the three that can express two different widths for the same type at the same offset — which is precisely what you need in order to relocate a variable-width field.r_extern is a fourth meaning of a name. When it is 0, r_symbolnum is not a symbol index at all — it is a section number. The single .data relocation shows this: addr = 0x08, word = 0x06000003, giving r_extern = 0 and r_symbolnum = 3 — and llvm-readobj-21 reports its target as the section__cstring, not a symbol. One field, two namespaces, selected by one bit. Compare ELF, which solves the same problem with a reserved st_shndx value and a symbol type, and COFF, which has IMAGE_SYM_STATIC versus IMAGE_SYM_EXTERNAL.__text offsets in file order are 0x37, 0x26, 0x0e, 0x08 — decreasing. ELF and COFF both store relocations in increasing offset order. This is not an artifact; it is a real Mach-O invariant, because linkers have historically walked relocation entries backwards while laying a section out. A tool that assumes ascending order across all three formats produces correct-looking output on two of them and silently wrong output on the third.The same four holes in .text, as the three formats record them. The columns are the four questions, and the rows are the three answers:
text off ELF COFF Mach-O
-------- ------------ --------------- ---------------
0x04 PC32 REL32 SIGNED
sym 5 sym 17 sym 2 _global_counter
addend -4 (none) len 2, pcrel
0x0a PC32 REL32 SIGNED
sym 6 sym 18 sym 5 _uninitialised
addend -4 (none) len 2, pcrel
0x21 PLT32 REL32 BRANCH
sym 8 sym 20 sym 7 _external_fn
addend -4 (none) len 2, pcrel
0x33 PC32 REL32 SIGNED
sym 10 sym 22 sym 3 _message
addend -4 (none) len 2, pcrel
Now read the table as a linker author and the design differences become requirements rather than trivia. Four things had to be decided, and the three formats decided them differently:
The fourth row is the one to carry away. ELF lets the producer choose — RELA puts the addend in the record, REL puts it in the section bytes — which is a genuine architectural freedom with a real cost, since a linker must support both. COFF and Mach-O have no choice to make: the addend is always in the bytes. And that is exactly why, in demo_coff.o, the four bytes at .text offset 4 are 00 00 00 00 rather than fc ff ff ff: the bias that ELF stores as an addend of -4, COFF applies from the definition of REL32 itself. The next concept proves that by linking the object and watching where the -4 appear.
$ cd courses/obj/assets/samples
$ xxd -s 0x240 -l 48 demo_elf.o # the four .rela.text records
$ xxd -s 0x16a -l 40 demo_coff.o # the four COFF records
$ xxd -s 0x3b8 -l 32 demo_macho.o # the four Mach-O recordsreadelf, llvm-readobj-21 and llvm-objdump-21. Start with the Mach-O word values, because they are the only ones with a real bit layout: confirm that 0x1d000003 unpacks to the five fields listed above. Then deliberately unpack one bit wrong — read r_length as 3 bits instead of 2 — and see what number you get and why it is not obviously wrong.r_info changes. The PLT32 record at offset 0x21 has r_info = 0x0000000800000004: the symbol is 8 and the type is 4. Write the formula r_info = (sym << 32) | type from the evidence, then check it against a 32-bit record from demo_i386_nopic.o where the split is 8 and 24 instead of 32 and 32.demo_macho.o has. Build one that needs eight or ten, then confirm they are still stored in decreasing offset order while the same relocations in the ELF build of the same source are in increasing order. Also confirm the offsets really are addresses, not section offsets, by finding a Mach-O object where a data section's relocation offset is larger than the section's size — it can only be an address.REL32-for-everything claim against the i386 target. Build the same source for i386-pc-windows-msvc and look at the types. On i386 the call and the data references get different relocations, so “COFF has no PLT variant” is a statement about x86-64 and not about COFF — which is a sharper version of the lesson, and worth finding out for yourself.python3 crosscheck.py decodes these files with a reader written from the specifications and compares the result against readelf and llvm-readobj-21 field by field. It currently reports 95 checks passing. Break one deliberately — flip a section's sh_info in a copy of demo_elf.o — and find which tier catches it. The answer is Tier 3, the file disagreeing with itself, and that is the tier that catches the most.Without looking back: a Mach-O relocation record is 8 bytes, and 4 of them are a single 32-bit word holding six distinct facts. ELF's equivalent record is 24 bytes holding three fields. What does the Mach-O layout buy, what does it cost, and which of the four questions does ELF answer more clearly than Mach-O does?
You are writing the reader for a tool that must open ELF, COFF and Mach-O objects. You have implemented ELF and it works on every file you have tried. Someone hands you a demo_coff.o and your tool reports that the first relocation in .text is of type 0x11 — seventeen — against symbol 4, at offset 4. Nothing crashes. The output looks like a plausible disassembly with three broken references. What exactly did your ELF assumption do, and what is the general rule that would have caught it before the file was opened?
The general habit: identify the format from the container header once, before parsing anything, and never infer it from a record you have already misread. Records in all three of these formats are unaligned, unchecksummed and self-describing only in the sense that a field's width tells you nothing about its meaning.
This concept is the deep dive that the two before it were leading to. A Hole and a Record introduced the four questions and worked one record. The Object Relocation Sets then took the third question — the arithmetic — and showed that its 44 and 147 entries are seven property flags rather than a list to memorise. This is the record those flags live in, and the finding is that the record's layout is a separate design axis from its vocabulary. A linker must get both right, and conflating them is why tools that handle two formats usually get the second one subtly wrong.
The r_addend field is a thread straight into the next concept, which takes the -4 in this record apart. The important thing established here is only that in an ELF64-RELA file the -4 lives in the record and the four bytes in .text are zero — a claim that surprises most people and that the next concept verifies by building the 32-bit counterpart, where the -4 really is in the bytes, and by linking the COFF object to show the bias appearing from nowhere.
The r_extern bit connects to Symbol Tables, Compared, and the connection is the section-symbol question that concept raised. Mach-O's answer is that section symbols are unnecessary — one bit in each relocation says whether the index is a symbol or a section — whereas ELF spends a 24-byte symbol table entry per section that something needs to name, which is exactly why demo_elf.o has two of them and Mach-O has none. Three formats, one question, and the cheapest answer is the one that reuses a field that was already there for something else.
The r_length field has a surprising consequence that belongs to a course you have not reached yet. A relocation whose width is stated in the record can patch a variable-width field. That is what the WebAssembly course'sR_WASM_MEMORY_ADDR_LEB does, and it is strictly harder than a PE base relocation or an ELF PC32 because getting the width wrong yields a misaligned instruction stream rather than a wrong address. The bit that looks like a size optimisation is the thing that makes a whole class of relocation possible, and it is there because the designers had a need for it that we can only see from outside.
And the section-association choice connects to No Segments, and Why. All three formats answer “which section does this relocation belong to” without a segment table, because an object has no segments. ELF puts the answer in a section header, COFF and Mach-O put it in a pointer, and all three are answering a question an executable would answer with PT_LOAD or LC_SEGMENT_64. That is the same fact as the missing segment table, seen from the other end: the object file is not an image with the mapping left out, it is a different kind of file that never had one.
Next: where the addend lives, which turns out to be a choice ELF makes per file and the other two formats do not make at all.