The second concept introduced a relocation record and worked through one of them. It did not tell you how many there are, and the honest answer is that the number is the first thing that makes the mechanism look impractical:
X86-64 44 relocation types AArch64 147 for LP64, plus 86 more for the ILP32 ABI = 233
Forty-four sounds like a lot to implement. It is not, and the reason it is not is the single most useful thing in this concept: the table is not a list, it is a classification. Every entry carries a small set of property flags, and an implementation switches on the flags rather than on the name. Forty-four entries collapse to about seven distinct behaviours.
This matters for the mission directly. A learner building a linker has to face this table eventually, and the difference between "147 cases to memorise" and "seven properties to combine" is the difference between a course that can be finished and one that cannot. So this concept is about the structure of the table, not its contents — and the contents are enumerated anyway, because the mission says teach everything, with a clear statement of which parts a linker must implement and which it can defer.
First, where the authoritative list actually comes from, because that is a practical question with a specific answer. Two sources, and they must agree:
That asymmetry is worth dwelling on, because it is a general lesson about where format truth lives. elf.h tells you the names and numbers. The .def file tells you the semantics. And the reason is that the names are what a programmer writes while the flags are what an implementation needs — so LLVM's table is shaped by the requirement that a code generator be able to switch on a property rather than match 147 strings.
The flags, decoded, and this is the whole concept in one table:
flag bit name what it tells an implementation
---- --- --------- ------------------------------------------
REL 0x1 Relative divide by the field width: the value
already stored in the bytes is an
offset, not a final value
SYM 0x2 Symbolic the result is a plain symbol address,
NOT divided by anything
PC 0x4 PCRelative subtract the patch address
TYPE 0x8 Type the field holds an ADDRESS, so the
result must fit in the field or the
link must fail
SIZE 0x10 Size the relocation needs TWICE the field
space, because a second quantity has
to be encoded alongside
TLS 0x20 TLS the symbol is thread-local
IREL 0x40 Indirect the value is a resolver function's
address, patched at load time
Every one of those seven is a thing a linker has to decide, and the table exists so the linker does not have to decide it by matching names. That is the whole design. A linker implements switch (flags & PC) and switch (flags & TYPE) and switch (flags & REL), and the forty-four names never appear in the code.
The counts, and then the twelve entries a linker implements first, with their flags decoded from the .def file on this machine:
ARCH TOTAL PC-RELATIVE TLS ------ ------ ----------- ---- X86-64 44 18 18 AArch64 147 71 53 (LP64) AArch64 86 - - (P32 / ILP32, separate ABI) AArch64's 233 is two ABIs in one table, which is worth knowing before you conclude anything from the number.
And the twelve that matter first, on x86-64:
TYPE NUM FLAGS what it does
--------------------------- --- ----------- ---------------------
R_X86_64_NONE 0 none a placeholder; do nothing
R_X86_64_64 1 REL S + A, 8-byte field
R_X86_64_PC32 2 SYM S + A - P, 4-byte
R_X86_64_GOT32 3 SYM+REL offset from the GOT
R_X86_64_PLT32 4 PC PC-relative, and
a call: may need a PLT
R_X86_64_GOTPCREL 9 TYPE+REL address of a GOT slot
R_X86_64_32 16 SIZE absolute, must FIT
R_X86_64_32S 17 SIZE+REL signed absolute
R_X86_64_16 18 SIZE+SYM 16-bit absolute
R_X86_64_8 20 SIZE+PC 8-bit PC-relative
R_X86_64_PC64 36 TLS+PC PC-relative, 8 bytes
R_X86_64_GOTOFF64 37 TLS+PC+REL offset from the GOT
Three things in that table are worth more than the rest.
The name says PC-32 and the flag says SYM. That looks like an error and it is not — the flags are LLVM's implementation classification, and they do not attempt to restate the mnemonic. The number after the mnemonic is the field width; the flags are the behaviour. Two different facts, two different places, and confusing them is the most common way to misread this table.
This is the closure of the finding from the second concept, and it is satisfying. That concept measured that R_X86_64_PC32 and R_X86_64_PLT32 perform identical arithmetic, with the only difference being a hint that a PLT entry might be needed. Here is where the difference lives: PLT32 is flagged PC and PC32 is not.
So the hint is not a separate boolean. It is the PC flag itself, and the reason is that on x86-64 a call is genuinely PC-relative in a way a data reference is not — which is also why PLT32 is the type that needs a PLT and PC32 is not. The mnemonic says PC and the flag says SYM because the name describes the field and the flag describes the relocation. Read both, and you have the whole type.
Now the one that explains the AArch64 doubling, and it is the best evidence in the whole course that the pairing is a real constraint rather than a convention:
R_AARCH64_ADR_PREL_PG_HI21 275 SIZE+SYM+REL R_AARCH64_ADR_PREL_PG_HI21_NC 276 SIZE+PC R_AARCH64_ADD_ABS_LO12_NC 277 SIZE+PC+REL R_AARCH64_LDST32_ABS_LO12_NC 285 SIZE+TYPE+PC+REL R_AARCH64_LDST64_ABS_LO12_NC 286 SIZE+TYPE+PC+SYM R_AARCH64_CALL26 283 SIZE+TYPE+SYM+REL R_AARCH64_JUMP26 282 SIZE+TYPE+SYM R_AARCH64_ADR_GOT_PAGE 311 TLS+SIZE+PC+SYM+REL R_AARCH64_LD64_GOTOFF_LO15 310 TLS+SIZE+PC+SYM
Every single AArch64 relocation that participates in an ADRP/LO12 pair carries the SIZE flag. And the flag's meaning is "this relocation needs twice the field space, because a second quantity has to be encoded alongside."
That is the machine-checked form of the finding from the second concept. One reference to an address on AArch64 needs two relocations because the page delta and the page offset are two separate fields, and LLVM flags both halves as needing extra space precisely because they will have to be combined with their partner. The pairing is not a convention a code generator and a linker agree on by documentation; it is recorded in a bit that says this one is not independent.
SIZE flag is the format telling you that independence is the wrong model, and it is telling you in the only channel available: a bit in a table.What a linker actually has to implement, which is a much shorter list than either table, and it is worth writing down because the gap between "147 types" and "nine cases" is the difference between a feasible and an infeasible project:
Which is the honest answer to "how do I implement 147 relocations", and the answer is: you implement nine and derive the rest. Step 5 deserves emphasis because it is the one a beginner forgets and the one that produces the worst class of bug: the TYPE flag exists because a 32-bit absolute relocation to an address outside 32 bits is not a truncation, it is a failure. A linker that silently truncates produces a binary that runs and reads the wrong memory; a linker that fails produces an error you can act on. Every SIZE-flagged absolute entry in the table — R_X86_64_32, R_X86_64_16, R_X86_64_8 — is a place where that check is mandatory, and they are flagged SIZE precisely so the check cannot be forgotten.
Where the table is genuinely large is TLS, and the numbers explain why: 18 of x86-64's 44 entries and 53 of AArch64's 147 are TLS. Thread-local storage is not one relocation, it is a family of about a dozen per architecture, because a thread-local address has several components — the thread pointer, an offset into the TLS block, a module offset, a symbol offset — and each instruction form combines a different subset. Compare that with the four non-TLS absolute and PC-relative cases. Relocation table size tracks instruction-set complexity in the area being addressed, not the number of addressing modes.
Which is why "read the waste to read the history" has a limit worth stating. A 44-entry table is not 44 design decisions; it is about nine decisions expanded by field width, by sign, and by TLS. And the TLS entries are the newest — they accumulate as thread-local models were standardised across architectures, which is the same pattern as the JVM attribute mechanism and the ELF section types. A format's table is a growth record, and the growth clusters where new capabilities arrived rather than spreading evenly.
Finally, the question a learner will ask and the answer is worth having ready: which of these are object-only? The answer is that the table is shared. The same R_X86_64_PC32 appears in a relocatable object and in a linked executable, and R_X86_64_32 is common in objects and forbidden in position-independent executables — where a build with -fPIE has no absolute relocations at all, and the presence of one is what control flow hardening checks for. So the table is not partitioned by file type; the compiler's choices determine which subset appears, and a linker must handle the union. That is a design fact worth holding, because it means a linker cannot assume it will only ever see the narrow set, and a PIE-aware linker has to reject the wide one deliberately rather than by omission.
$ grep -c R_X86_64_ /usr/include/elf.h
$ grep -cE '^ELF_RELOC' /usr/lib/llvm-21/include/llvm/BinaryFormat/ELFRelocs/AArch64.defelf.h, 233 from AArch64.def — then find the 86 R_AARCH64_P32_ entries and subtract them to get 147 for the LP64 ABI. The headline number is 233 and the useful number is 147, and the difference is a second ABI hiding in the same table. Always check for that before drawing a conclusion from a count.R_X86_64_ names from elf.h and from X86_64.def and diff them. Any name in one and not the other is a version skew, and finding one is how you learn that the two files can disagree — which is exactly the situation the mission's verification rule exists for.SIZE-flagged entry and check whether it pairs. On AArch64, list them and look for the ADR_PREL_PG_HI21/LO12_NC pattern. Then compile a file that takes the address of a global on AArch64 and confirm you get two records for one reference — the concept's central claim, reproduced from scratch.-fno-pic, and try to link it into a PIE. The TYPE flag exists so this fails loudly, and watching it fail is worth more than reading that it would. Then find which flag on which entry made the decision.readelf -r on a -fPIE build: expect none of the _32 family. Then build with -fno-pic and count them again. The difference is the entire practical meaning of the PC-relative and GOT-relative entries, measured rather than argued.Without looking back: an AArch64 object has R_AARCH64_ADR_PREL_PG_HI21_NC at offset 0 and R_AARCH64_LDST32_ABS_LO12_NC at offset 4, for one reference to a global. What does the SIZE flag on both of them tell a linker, what happens if the linker processes them independently, and why is the failure a run-time fault rather than a link error?
You are writing the relocation-application pass of a linker. It reads each record, computes the value from the type's flags, and writes it, in list order, with no interaction between records. It is correct on every x86-64 object you have tested. On AArch64 it links without error and every pointer to a global is wrong, while calls to functions are correct. The SIZE flag is right there in the table and your code ignores it. What is the smallest change that makes the pass correct, and why is a per-record fix impossible rather than merely tedious?
The general habit: check the flags that say a record is not independent before deciding your pass can be a single loop over records. A record that needs a partner does not contain enough information to produce the right answer on its own — that is a stronger constraint than a missing ordering.
This concept is the payoff for the one before it and the one after. A Hole and a Record established that a relocation answers four questions and that the third — the arithmetic — is a small per-architecture enum. This is that enum, and the finding is that it is not an enum at all: it is a set of independent properties. That reframing is worth more than the table itself, because it is the difference between memorising 147 names and implementing seven flags.
The ELF course's relocation types concept teaches the same table for a single architecture, and the honest division of labour between the two is worth stating: that concept is the reference, and this one is the structure. You need this one to write a linker and the other one to read a manual. And the AArch64 pairing it explains is the same fact the second concept measured as "eight records where x86-64 needs four" — here it is a bit in a table rather than an observation about a file, which is the stronger form of the same claim.
The TYPE flag and the overflow check connect straight to the collapsed Relocations, PIC and PIE course, and to a security consequence rather than a correctness one. A 32-bit absolute relocation is incompatible with position independence, because the whole point of PIE is that the load address is not known until run time. So a PIE-aware toolchain must emit no absolute relocations, and the hardened-executable checks look for their presence as a signal that a binary is not position-independent. A flag in a relocation table is therefore also a security property, and that is the moment the format stops being about addresses and starts being about trust.
The TLS entries connect to the COMMON concept in an unexpected way, and it is worth naming. Both mechanisms exist because C has a declaration whose storage is decided somewhere other than where it is written: COMMON because the definition might be elsewhere, TLS because the storage is per-thread and therefore cannot be assigned at link time at all. One is resolved by merging and the other by indirection through a thread pointer, but both are the same underlying problem — a symbol whose address is not a constant, and therefore cannot be the answer to any relocation's arithmetic. A linker that implements COMMON and TLS is implementing the same idea twice, in two vocabularies.
And the growth story is the format-history story again, at a larger scale. 18 of x86-64's 44 entries and 53 of AArch64's 147 are TLS — a single capability, arriving after the core table was designed, and expanding it by a factor of three. That is precisely the pattern of the JVM's attribute mechanism, of ELF's section types, and of the mission's own observation that formats grow additively. A relocation table is a record of what the instruction set ended up needing, not of what anyone planned, and reading the distribution tells you which capabilities were hard to retrofit. On both architectures the answer is thread-local storage, which is a genuinely useful thing to know and is not written down anywhere in the specifications.
Next: position independence, which is where this table stops being a reference and becomes a design tool — because choosing which of these types to emit is what position independence is.