The previous concept decoded a relocation record and found an r_addend of -4 sitting next to four zero bytes in the section it patches. That looks contradictory. It is not, and the reason is the single most consequential difference between the three formats in this module: two of them put the addend in the record, one puts it in the bytes, and a third hides it in the relocation type's definition.
Here is the whole problem in one command's output. This is real, from llvm-objdump-21 -r on the staged specimen:
RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000004 R_X86_64_PC32 global_counter-0x4 0000000000000021 R_X86_64_PLT32 external_fn-0x4 0000000000000033 R_X86_64_PC32 message-0x4
That -0x4 is not part of any symbol name, and reading it as one is the mistake almost everyone makes the first time. It is the addend, and the disassembler is showing you a value that is not in the file at all — it is in the relocation record, which the disassembler is choosing to display next to the name.
-0x4 lives in the four bytes of .text at offset 4, because that is where a displacement normally goes. In demo_elf.o those four bytes are 00 00 00 00. The -4 is in r_addend, in the record, and the bytes are zero because a relocatable object has no addresses to put there. Building the 32-bit counterpart of the same source — which uses REL instead of RELA and therefore has no addend field — is what makes the contrast visible, because there the -4 really is in the bytes.Start from the arithmetic, because the addend only makes sense as part of it. The general shape of a relocation's value is:
A exists because S - P is not always the whole answer. Three reasons a format needs it, in increasing order of how often they bite:
P is the field's address, which is inside it. The distance from the field to the instruction's end is a property of the encoding, known at compile time and identical for every single PC-relative relocation in the file. This is the -4, and it is by far the most common addend you will meet.int *p = &arr[3]; needs A = 12. The symbol gives you arr; the constant 3 is the programmer's, and nothing else in the file records it.Now the design question, and it has exactly two answers a format can give:
The consequence worth understanding is this: under the implicit scheme, the section bytes are wrong until the link happens. They contain a value computed against a section-relative symbol address that is only correct if the symbol stays exactly where the producer put it. Move the section during linking and every pre-filled field is stale. Under the explicit scheme the bytes are simply zero, which is never wrong, and the linker writes the real value once.
demo_elf.o's four .text records are 96 bytes where the REL form would be 32. It buys correctness under section reordering, and it is the reason a 64-bit ELF object is measurably larger than the 32-bit one for the same source. The trade was made in favour of explicit, and modern 64-bit targets use it universally. The 32-bit x86 target still does not, which is why the same source produces REL records there and the contrast is still observable on any machine.The first .rela.text record of demo_elf.o and the four bytes it patches, side by side. Bytes from the file at 0x40 (.text) and 0x240 (the record):
.text at 0x40 the record at 0x240
00000040: 01ff 033d 0000 0000 00000240: 0400 0000 0000 0000
^^^^^^ 0200 0000 0500 0000
the four bytes fcff ffff ffff ffff
the patch targets ^^^^^^^^ the -4, as a signed i64
These are EIGHT ZERO BYTES AT DIFFERENT PLACES. The
displacement field is zero and the addend is -4. Nothing
in the section says what the displacement should be; the
record says what to add to it.
So the answer to "where does the -0x4 the disassembler printed come from" is: from r_addend at offset 16 of the record, and from nowhere else. And readelf -rW shows both halves at once, which is the clearest single view of the concept:
Offset Info Type Symbol's Value Symbol's Name + Addend 0000000000000004 0000000500000002 R_X86_64_PC32 0000000000000000 global_counter - 4 000000000000000a 0000000600000002 R_X86_64_PC32 0000000000000000 uninitialised - 4 0000000000000021 0000000800000004 R_X86_64_PLT32 0000000000000000 external_fn - 4 0000000000000033 0000000a00000002 R_X86_64_PC32 0000000000000008 message - 4
Read the columns as the three quantities of the model. "Symbol's Value" is S, and it is 0 for the three undefined symbols and 8 for message — because in a relocatable object a defined symbol's value is an offset within its section, not an address. "Addend" is A. And the offset column is where the bytes are, not what they contain. All three are things a reader must keep apart, and the next section is about exactly how they get confused.
Same source, 32-bit target, -fno-pic so the encodings are the simple absolute ones. demo_i386_nopic.o's .text, 0x40 bytes at file offset 0x40:
00000040: 8b44 2404 01c0 0305 0000 0000 8b0d 0000
00000050: 0000 01c8 83c0 03c3 0f1f 8400 0000 0000
00000060: e9fc ffff ff66 662e 0f1f 8400 0000 0000
00000070: a100 0000 000f be00 83c0 69c3
^^
e9 = jmp rel32, opcode at text offset 0x20
fc ff ff ff = the addend, at text offset 0x21
And the relocation record for it — note there is no third field, because there is no such thing as an addend field in a REL record:
00000008 00000802 R_386_PC32 external_fn
+0 r_offset u32 = 0x00000021 <- points AT the fc ff ff ff
+4 r_info u32 = 0x00000802 <- type 2, symbol 8
<- 8 bytes total. No addend.
There is the contrast, in two files produced from the same 30 lines on the same machine. x86-64 stores the -4 in a 24-byte record and leaves the bytes zero; i386 stores the -4 in the bytes and uses an 8-byte record with nowhere to put it. Everything else about the mechanism is identical, which is what makes it a clean experiment: the difference is the format's choice, and both are correct ELF.
Worth noting what the other two relocations in that file do. addl $disp32, %eax at text offset 6 has a disp32 of 00 00 00 00 and an R_386_32, which is absolute and therefore needs no bias. Only the PC-relative one carries -4, and only because P is inside the instruction.
This is the case that surprises people, because COFF has no addend field and the bytes are zero. demo_coff.o's .text at file offset 0x12c:
0000012c: 01c9 030d 0000 0000 8b05 0000 0000 01c8
^^^^
the four bytes the REL32 at offset 4 patches:
all zero, and the record has no addend field
So where is the -4? In the definition of IMAGE_REL_AMD64_REL32, which the PE/COFF specification defines as the 32-bit displacement from the byte following the relocation. The bias is a property of the relocation type, applied by the linker every time, for free, with nothing in the file to store it.
This is verifiable, and the way to verify it is to link the object and look at what appears. demo.c compiled for i386-pc-windows-msvc, linked with ld -m i386pe --oformat pei-i386 against a second object defining external_fn:
BEFORE, in the object: AFTER, in the linked PE:
00401020: e9 00 00 00 00 00401020: e9 1b 00 00 00
the field was zero. the field is now 0x1b = 27.
where did 27 come from?
S = 0x401040 (_external_fn)
P = 0x401021 (the field's own address)
the bias, from REL32's definition, is 4 (to the byte AFTER)
0x401040 - (0x401021 + 4) = 0x401040 - 0x401025 = 0x1b
The -4 is nowhere in the input file, and the output has it. That is the proof, and it settles a question the ELF cases cannot: in RELA the addend is data, and in COFF it is code. A tool that reconstructs an object file from a linked image has to re-derive COFF's addend from the type, and cannot get ELF's without having kept the records.
demo_macho.o's __text at file offset 0x2c0, and all four relocation offsets are zero:
000002c0: 5548 89e5 01ff 033d 0000 0000 8b05 0000 000002d0: 0000 01f8 83c0 035d c30f 1f80 0000 0000 000002e0: 5548 89e5 5de9 0000 0000 660f 1f44 0000 000002f0: 5548 89e5 488b 0500 0000 0fbe 0083 c000 00000300: 695d c3 offsets 0x08, 0x0e, 0x26, 0x37 -> 00 00 00 00 every one the records have no addend field, and r_pcrel = 1
So Mach-O is in the same position as COFF: no addend field, zero bytes, and the correction must come from the relocation type. What I can and cannot claim here matters, so it is worth being exact. The Apple documentation defines X86_64_RELOC_SIGNED and X86_64_RELOC_BRANCH as a displacement measured from the end of the instruction, which is the same convention REL32 uses — so the bias is applied by ld64 from the type. I have not measured that, because there is no Mach-O linker on this machine and binutils refuses the format. The ELF and COFF claims above are measured; this one is documented, and the honest thing is to say which is which rather than present all three as equally established.
Three things get printed in a relocation listing, and on this course's specimens all three look like "the value". Only one of them is. Here are all three, for the same relocation, from three tools:
THE RELOCATION: .text + 4, R_X86_64_PC32, against global_counter
tool what it prints what that actually is
---------------- ------------------------ ---------------------------
readelf -rW Symbol's Value 0000...0 S: the symbol's st_value,
Name + Addend 0 because it is UNDEFINED
llvm-objdump -r global_counter-0x4 A: the addend, concatenated
onto the name for brevity
xxd on .text 00 00 00 00 the field, currently ZERO
and the record:
xxd on .rela ... fcff ffff ffff ffff A: the addend, for real,
as a signed i64
Three different numbers, one relocation, and the concatenation is the trap.global_counter-0x4 is not a symbol whose name contains a minus sign; it is a name and a value with no separator, because a disassembler's column is narrow. A tool that parses that column as a name gets a symbol called global_counter-0x4, fails to resolve it, and reports an undefined reference to a symbol that does not exist.
The same three-way confusion has a nastier variant, and it is the one that costs people an afternoon. Consider a relocation whose symbol is defined:
THE RELOCATION: .rela.data + 8, R_X86_64_64, against .rodata.str1.1
readelf: 0000000000000000 .rodata.str1.1 + 0
Here Symbol's Value is 0 AND the addend is 0. Two different
zeros that mean different things:
S = 0 the section symbol's st_value, an offset within
the section -- correct, and not an address
A = 0 the addend
A reader that collapses "S + A" into one number and reports
it as an address will print 0 for the address of a string
literal. In a relocatable object that is not wrong -- there
are no addresses -- but it is not an answer either, and in
a linked image the same computation must have picked up the
section's base address from somewhere.
And there is a third trap in the same area, which the COMMON concept found and which belongs here because it is the same confusion wearing a different hat. A SHN_COMMON symbol's st_value is an alignment, not a value. So S is not an address, not an offset, and not a size — it is a constraint on where the linker may put the object. Which of the three things S means is decided by st_shndx, so the meaning of st_value is a three-level dependency: read the index, and only then know what the value is. A relocation against a COMMON symbol is the one case where the arithmetic S + A - P has to be deferred until the linker has allocated storage, and no field in the record tells you that — you have to look the symbol up.
$ cd courses/obj/assets/samples
$ xxd -s 0x40 -l 8 demo_elf.o # the field: zero
$ xxd -s 0x250 -l 8 demo_elf.o # the addend: fffffffffffffffc
$ xxd -s 0x60 -l 6 demo_i386_nopic.o # the field: fcffffff <- the -4demo_elf.o's .text+4 is 00 00 00 00 while its r_addend is fc ff ff ff ff ff ff ff, and that demo_i386_nopic.o's .text+0x21 is fc ff ff ff with no addend field anywhere in its 8-byte record. Same source, same machine, two answers — and the only variable is whether the format's record type is REL or RELA.Elf64_Rela is 24 bytes and an Elf64_Rel is 16. Count the relocation records in demo_elf.o and in demo_i386_nopic.o and compute what the records alone cost in each. Then notice that the explicit form also makes the section smaller, because the bytes are zero rather than a pre-filled value — so the saving is not the whole story either.int *p = &arr[3]; with int arr[8]; and compile it for both targets. The x86-64 record will carry A = 12; the i386 file will have 0c 00 00 00 in the bytes at the field. Then find a PC-relative relocation in the same file and confirm it is still -4, which shows the two reasons for an addend coexisting in one file.clang -target i386-pc-windows-msvc -O1 -c demo.c, then ld -m i386pe --oformat pei-i386 -o out.exe a.obj b.obj against an object defining external_fn, then llvm-objdump-21 -d out.exe. Write down the four numbers — S, P, the bias, the result — and confirm the result appears in the output although nothing in either input file contained it. This is the single most convincing demonstration in the module.-0x4 parsing trap in the wild. Run llvm-objdump-21 -r and readelf -rW over several objects in this course's sample directory, and write the parser for each that correctly separates name from addend. Then try the naive one — take the last whitespace-separated field as the symbol name — and count how many objects it breaks on. Every relocation with a nonzero addend breaks it, which on real code is most of them.Without looking back: you are given a COFF object whose .text has a 00 00 00 00 at the offset of an IMAGE_REL_AMD64_REL32 record, and that record has three fields — offset, symbol index, and type — and no addend. After a successful link the four bytes hold a non-zero displacement. The input file contained no addend and the output has one. Where did the -4 that made the arithmetic come out right come from, and what does that tell you about what a tool can and cannot recover from which format?
You are writing a tool that converts object files: it reads an ELF object, applies relocations itself, and writes a new object file that should be byte-comparable to the input where nothing needed changing. It works on ELF64-RELA input. On ELF32-REL input it produces a file that links and runs correctly, but which is not byte-comparable — the .text section differs, and the relocation sections differ in size. On COFF input it produces a file that links and runs correctly and is not byte-comparable either. Diagnose both, and say which of the three differences a tool could in principle eliminate.
The general habit: before you can decide whether a format is round-trippable, find out whether its constants are on disk. That single question separates the three cases, and it is the difference between a format you can rewrite and one you can only re-derive.
This concept is the second half of the fixup record, and the two are one idea seen at two magnifications. That concept asked what a record contains; this one asks where the constant goes, and the answer is that the format's choice about the record's size is really a choice about where the arithmetic's constant lives. 24 bytes versus 8 bytes is not a compactness decision, it is a decision about whether the section bytes are allowed to be wrong before the link.
The bias connects to A Hole and a Record and to the course's very first measurement. The finding there was that the four relocation offsets land on the trailing disp32 of instructions of three different lengths — a 6-byte, a 6-byte, a 5-byte and a 7-byte encoding — and every one of them has the same -4. That sameness is the bias being a property of the encoding rather than of the reference. The moment an architecture has an instruction whose displacement field is not four bytes before the end, the bias changes, and on AArch64 it is not even a single number: the ADRP/LO12 pair that the relocation table concept describes carries a 21-bit page delta split across bits 30:29 and 23:5. The encodings concept takes that apart, and it is where this concept's arithmetic stops being simple.
The COMMON connection is the sharpest one, and it is a three-level dependency worth restating because it is easy to miss. A SHN_COMMON symbol's st_value is an alignment. So in the formula S + A - P, the S term is not a position at all — and which of the three meanings it has is decided by st_shndx, whose reserved values are the special case. The COMMON concept measured all five builds; this concept names why the result matters: a relocation against a COMMON symbol cannot be computed at object-write time, because the storage does not exist yet. It is the same family of problem as an undefined weak, which Module 4 handles, and both are cases where the format must record a promise rather than a value.
On the portability side there is a real consequence that connects to the PE course. A format that keeps its constants on disk is auditable; a format that keeps them in its own specification is not. That sounds like a distinction without a difference until you try to write a validator, or a tool that must transform an object without linking it — an obfuscator, a coverage-instrumentation pass, a binary patcher. For ELF-RELA the answer is on the disk. For COFF it has to be re-derived from the type, every time, and a validator that forgets the bias for one type will report a well-formed file as corrupt. That is the practical difference between storing a constant and defining it, and it is the difference between a property you can read and a property you have to know.
Finally, the three-way confusion in the listing output connects to Symbol Tables, Compared, because all three of those columns come from two different tables that the listing interleaves. A relocation listing is a join of the relocation table and the symbol table, and the join is where the confusion lives — a tool that displays them side by side without separators has made the reader do a disambiguation the format never asked for. Next: the type vocabulary those S + A - P formulas are drawn from.