Relocations: fantastic symbols, but where to find them?
Posted: Apr 10, 2020, 12:36Update: There’s a Links section at the end that should give you a list of all the reference you’ll ever need to undrstand Linkers and Relocations!
When I started out in compilers years ago, I found relocations especially hard to wrap my head around. They’re just simple math in the end but they combine elements from different places that make it complicated enough that many (as did I back then) assume it to be some kind of black art. The Oracle documentation on relocation processing and relocation sections is pretty much the best thing I’ve found on the internet that explains relocations and they’re a great start if you already know what you’re doing. This is why I figured I ought to try writing something more accessible that puts some of the bits into practice. The fact that I’m currently working on relocation processing makes it that much simpler for me to just bash at the keyboard and commit the stuff in my memory to a more persistent medium before it gets swapped out to make space for kitten photos.
While this tutorial is meant to be beginner friendly, it does assume though that the reader has some awareness of the ELF format, at least to the extent of knowing about different ELF sections. It also assumes that the reader has some undersanding of assembly language programming, bonus if you know aarch64 assembly since that’s the flavour of the examples.
Finding each other in this crowded world
The idea of relocation is quite simple: when compiling programs, we need flexibility to build components of programs independently and then have them link together. This could be in the same source file where we don’t know where parts of the source would end up, multiple source files built into different object files or sets of object files built into different libraries that reference objects in each other. This flexibility is achieved using relocations. Here’s a very simple example using aarch64 assembly:
.data
.globl somedata
somedata:
.8byte 0x42
.text
.globl start
_start:
nop
ldr x2, somedata
This is a simple program that loads somedata
into register x2
. It doesn’t do much and if you try to run the program it will crash, but it is a useful example that shows an assembly source where parts of it end up in different ELF sections.
The interesting bit here is a load instruction in the text section that is reading a variable somedata
from the data section. The load instruction encodes within itself, the offset of somedata
from itself, aka the PC-relative offset. The assembler can see both, the variable and the instruction but it cannot say for sure where they will be in memory at this point, because it does not know how far the data section will be from the text section in the final library or executable. To work around this limitation, it needs to leave the offset field in the ldr instruction blank so that the linker can finally fill it in. It also needs to provide instructions to the linker to tell it how to fill in this field.
This is where relocations come into play.
In this specific case, one may assemble the example and disassemble it using objdump -Dr
to find this disassembly:
Disassembly of section .text:
0000000000000000 <_start>:
0: d503201f nop
4: 58000002 ldr x2, 0 <_start>
4: R_AARCH64_LD_PREL_LO19 somedata
Disassembly of section .data:
0000000000000000 <somedata>:
0: 00000042 .inst 0x00000042 ; undefined
4: 00000000 .inst 0x00000000 ; undefined
The R_AARCH64_LD_PREL_LO19
in that output is the relocation. How did it land in there in between the instructions you ask? Well, it didn’t. The relocations are actually in a separate section of their own, as is evident with objdump -r
:
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000004 R_AARCH64_LD_PREL_LO19 somedata
even better with readelf -r
because it tells you that the relocations are essentially just a table of entries in the .rela.text
section:
Relocation section '.rela.text' at offset 0x110 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000004 000500000111 R_AARCH64_LD_PREL 0000000000000000 somedata + 0
All sections with relocation entries have names with prefix .rela
or .rel
followed by the name of the section for which the relocations need to be applied. Based on these section names, it’s evident that there are two types of relocation entries, REL and RELA. There are a number of important pieces of information the assembler leaves for the linker here:
- The memory address it needs to fix up
- Which symbol the memory address is referring to
- What is the offset from the symbol that it should finally consider as the result
- How should it perform the computation
All of this can be seen in the above relocation table. Each entry in the relocation table is basically a C structure of the following form for REL type relocations:
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
} Elf64_Rel;
and for RELA:
typedef struct {
Elf64_Addr r_offset;
Elf64_Xword r_info;
Elf64_Sxword r_addend;
} Elf64_Rela;
r_offset
corresponds to the Offset
entry in the readelf output above and is typically the memory address that needs to be fixed up. The offset from the symbol, aka the addendum is present only in RELA type relocations and it corresponds to the r_addend
element in the structure and the Addend
field in the readelf output. The symbol and computation related information is encoded in the r_info
field.
The Elf32_Rel
and Elf32_Rela
structures are similar, except for the data sizes of the elements.
Symbol hunting
The ‘what to write’ is where the r_info
field comes in. That’s what the linker needs to figure out before the where and how, which comes later.
This field is split into two 32-bit parts (16-bit for 32-bit architectures). The lower part tells the linker how to perform the computation and the upper part tells the linker what the target symbol is. The upper part is a symbol ID, which basically is an index into the symbol table. In our example above, the r_info
is 0x000500000111
, which means that the symbol id is 0x5. We can pull out the symbol table using readelf -s
:
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 3
3: 0000000000000000 0 SECTION LOCAL DEFAULT 4
4: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 $x
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 3 somedata
6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 _start
and we find that the symbol id 0x5 is somedata
and has the Value
(i.e. the address of the symbol relative to its section) of 0x0.
Now we need to figure out how to combine all of this information together using the lower part of r_info
, which is 0x111. This number corresponds to R_AARCH64_PREL_LO19
, which has a specific meaning. An architecture defines a number of such relocations with descriptions of how they’re supposed to be applied. In case of R_AARCH64_PREL_LO19
it means “Add the symbol address and addend and subtract from it, the location of the memory address that is being fixed up”. Think about that a bit and you’ll notice that it is how you would compute the PC-relative offset of somedata
from the instruction, i.e. subtract the location of the fixup (i.e. the LDR instruction) from the address of somedata
. In short form (and you’ll see this and similar notations to describe relocations), it is written as S + A - P
.
Putting things together
Now that we know what the target symbol is and how to compute the pc-relative offset, we need to compute the final symbol address, the final target address and then do the actual fixup. This is done near the end of the linking process in the GNU linker, when all sections have been laid out and we finally are in a position to know the relative addresses. The linker will then make the computation (i.e. S+A-P) and patch in the result into the LDR instruction before writing the output to the final binary. Here is what our result looks like:
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: d503201f nop
4000b4: 58080022 ldr x2, 4100b8 <somedata>
Disassembly of section .data:
00000000004100b8 <somedata>:
4100b8: 00000042 .inst 0x00000042 ; undefined
4100bc: 00000000 .inst 0x00000000 ; undefined
Notice that the opcode of the LDR instuction is now different (and as a result the instruction itself is also different) and includes the 0x8002, which is basically the encoded difference (0x10004) from somedata
.
Raising the stakes: Dynamic Relocations
This is all great when all our symbols are local and have predictable layouts such that PC-relative relocations such as R_AARCH64_PREL_LO19
are sufficient to describe and resolve in between assembling and linking a program. What happens however, when these symbol references cross boundaries of sections in ways that we cannot predict at compile or link time? What happens when your symbol references cross boundaries of your shared object? These are problems that need to be solved to make Position Independent Code (PIC) possible. PIC is when your program (and sections within your program) could get mapped anywhere in memory and you need your code to adapt to that fact.
Take this very simple example:
.data
somedata:
.8byte 0x42
somedata2:
.8byte somedata
.text
.globl _start
_start:
ret
There’s next to nothing here; just somedata
like in our previous example and a somedata2
which points to somedata
. However, in this next-to-nothing example lies an interesting complication that needs to be resolved at runtime: the value in somedata2
cannot be computed at compile time; it needs a fixup at runtime! Let’s walk through the compilation to see how we get to the final result. First, the disassembly to understand what the assembler did for us:
Disassembly of section .text:
0000000000000000 <_start>:
0: d65f03c0 ret
Disassembly of section .data:
0000000000000000 <somedata>:
0: 00000042 .inst 0x00000042 ; undefined
4: 00000000 .inst 0x00000000 ; undefined
0000000000000008 <somedata2>:
...
8: R_AARCH64_ABS64 .data
We see now that the address to be relocated is somedata2
in the .data
section and it is of type R_AARCH64_ABS64
. This is simple relocation that instructs the linker to compute S + A
to get the result, i.e. get the symbol address of somedata
and add the addendum (0 again in this case). This in fact would be the final result for a statically linked result (using ld -static
) and we’d lose the relocation in favour of the absolute address written into somedata2
:
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: d65f03c0 ret
Disassembly of section .data:
00000000004100b4 <somedata>:
4100b4: 00000042 .inst 0x00000042 ; undefined
4100b8: 00000000 .inst 0x00000000 ; undefined
00000000004100bc <somedata2>:
4100bc: 004100b4 .inst 0x004100b4 ; undefined
4100c0: 00000000 .inst 0x00000000 ; undefined
When compiling a shared object however (i.e. ld -shared
) we intend to produce a position independent DSO (dynamic shared object) and to achieve that the linker now emits a relocation to describe how to compute the final address to assign to somedata2
and where the memory address can be located. In this example, it is the R_AARCH64_RELATIVE
dynamic relocation, as seen using objdump -DR
(output snipped to retain only useful bits):
Disassembly of section .data:
0000000000011000 <somedata>:
11000: 00000042 .inst 0x00000042 ; undefined
11004: 00000000 .inst 0x00000000 ; undefined
0000000000011008 <somedata2>:
11008: 00011000 .inst 0x00011000 ; undefined
11008: R_AARCH64_RELATIVE *ABS*+0x11000
1100c: 00000000 .inst 0x00000000 ; undefined
This relocation is interesting not just for the reason that it is dynamic, but also because it is a S+A
type relocation that puts the non-relocated address (i.e. the link time address) of somedata
into its addend. This relocation also does not reference a symbol; instead it references an *ABS*
value, which is basically the offset at which this DSO would be loaded during execution. It is the dynamic linker in the C runtime library (ld.so in GNU systems) that reads these relocations from the .rela.dyn
section. Because this relocation is based on an absolute address computed by the static linker, the dynamic linker does not have to do a symbol lookup to resolve the relocation.
The other difference from static relocations is that when a dynamic relocation references a symbol in its r_info
, it is looked up in the .dynsym
section, i.e. in the dynamic symbol table and not in the regular symbol table.
Final Thoughts
There are a number of other cases that the linker needs to cater for when it comes to relocations such as entries in Global Offset Tables, resolution of intermediate functions and Thread-Local Storage. Thankfully though, the first principles behind all those relocations are the same as the above and you can apply this knowledge to GOT, TLS and IFUNC relocations as well. GOT relocations for example reference GOT base, which the linker knows where to find (since it sets up the .got
section in the first place) and can use that information to compute the location to fix up. Other than this special knowledge, everything else remains pretty much the same.
Once you’re equipped with these first principles, the next task is to figure out where documentation for specific relocations is for every architecture. While the binutils documentation makes some effort to document the public facing part of relocations, the detailed documentation is usually distributed by the architecture chip vendors. The AArch64 ELF documentation for example is hosted on the Arm website.
Links
- @gnutools pointed me to a full blog series by Ian Lance Taylor on Linkers, indexed by LWN
- @matt_dz maintains a comprehensive set of links on linking and loading on GitHub. This looks like everything you’ll ever need to understand ELF and linkers!