Back to course

The Addend Lives in the Bytes

20 minadvancedModule 3: The FixupContinues from obj-relocations
20 min · Module 3: The Fixup · Advanced

Why This Matters

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.

And here is the correction this concept exists to make, because it is the intuitive answer and it is wrong. The natural guess is that -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.

A Simple Model

Start from the arithmetic, because the addend only makes sense as part of it. The general shape of a relocation's value is:

value = S + A - P and then the field-specific transformation, if any S the address (or offset) of the symbol A the ADDEND: a constant that is not derivable from S P the address of the field being patched

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:

Now the design question, and it has exactly two answers a format can give:

IMPLICIT the section bytes already contain A. The producer writes S_local + A into the field, where S_local is the symbol's offset within its own section. The linker REPLACES the field with S + A - P. EXPLICIT the record carries A in a field of its own. The section bytes are left zero. The linker computes S + A - P and writes it. Both are correct. They differ in where the constant is stored, and that difference has real consequences in both file size and reader complexity.

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.

Which is why RELA exists and why it is not simply "better". The explicit form costs a field — 8 bytes per record on ELF64, so 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 Actual Detail

Case one: ELF64 with RELA — the addend is in the record

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.

Case two: ELF32 with REL — the addend is in the bytes

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.

Case three: COFF — the addend is nowhere, and the type supplies it

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.

Case four: Mach-O — the same situation, honestly stated

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.

A Real Example

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.

Try It Yourself

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

Check Your Understanding

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?

Apply It

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.

Connect

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.