Back to course

The Fixup Record

24 minadvancedModule 3: The FixupContinues from obj-bss-common
24 min · Module 3: The Fixup · Advanced

Why This Matters

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.

A Simple Model

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:

WHERE which bytes in which section get patched WHAT the arithmetic: what goes in them WHOM which symbol supplies a term HOW MUCH the constant part of the value ELF, COFF and Mach-O all encode exactly these four and nothing else. The interesting engineering question is not WHAT to store -- it is where to put each fact so the format does not have to be looked up.

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:

Mark the simplification here, because it is load-bearing. “Four questions” describes what a record must express, not what any format stores. No format has a field called 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 Actual Detail

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 packs two fields, and the halves are not symmetric

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 signed, and -4 is not a mistake

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.

The COFF record has three separate fields and no addend at all

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..........
+0 VirtualAddress u32 0x04, 0x0a, 0x21, 0x33 WHERE +4 SymbolTableIndex u32 0x11, 0x12, 0x14, 0x16 WHOM (17,18,20,22) +8 Type u16 0x0004 four times WHAT IMAGE_REL_AMD64_REL32

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.

The Mach-O record packs six facts into one 32-bit word

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:

bit 31..28 r_type = 1 X86_64_RELOC_SIGNED bit 27 r_extern = 1 the target is a SYMBOL, not a section bit 26..25 r_length = 2 the field is 1 shifted left 2 = 4 bytes bit 24 r_pcrel = 1 the value is RELATIVE to the field bit 23..0 r_symbolnum = 3 symbol 3 = _message

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.

A Real Example

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:

DECISION ELF COFF Mach-O ------------------- -------------- -------------- -------------- where to find the a separate inline after a pointer in relocations SHT_RELA the section the section section header header how to link a reloc sh_link + positional, positional, to its section sh_info nothing to nothing to record record how to pack sym+type one 64-bit word two fields one 32-bit word (sym high) (32 + 16) with 5 bitfields where the addend is a field, or in always in the in the section the section section bytes bytes

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.

Try It Yourself

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

Check Your Understanding

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?

Apply It

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.

Connect

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.