GOT and PLT
got plt
Relocations
Relocations are entries in binaries that basically tell the linker to put stuff there. It could be compile time linker for statically linked binaries or run- time linker for dynamically linked stuff.
It basically says, "Determine the value of X., and put that value at offset Y".
Link time example
extern int foo; int retfoo(void) { return foo; }
gcc -c a.c readelf --relocs ./a.o
Relocation section '.rel.text' at offset 0x2dc contains 1 entries: Offset Info Type Sym.Value Sym. Name 00000004 00000801 R_386_32 00000000 foo
On running objdump -D ./a.o
./a.o: file format elf32-i386 Disassembly of section .text: 00000000 <function>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: a1 00 00 00 00 mov 0x0,%eax 8: 5d pop %ebp 9: c3 ret
As you can see the value at 0x04
is 4 bytes of zeroes. These will be
resolved at linker phase, and the actual address will be added there.
Run time example
readelf --headers /lib/libc.so.6
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align [...] LOAD 0x000000 0x00000000 0x00000000 0x236ac 0x236ac R E 0x1000 LOAD 0x023edc 0x00024edc 0x00024edc 0x0015c 0x001a4 RW 0x1000
As you can see, the addresses of the library are not resolved until runtime. There is no specific base address. Only the offsets are specified.
Another goal is code sharing. We don't need 100 copies of the same library for 100 different processes.
At the same time, the data for each process attempting to access the shared library must be unique.
This is what is accomplished by the offset in the elf section. Note that this is virtual memory, so multiple processes can refer to the same address and still have separate data sections for themselves.
In AMD64
architecture you can directly move stuff with offset from
current pc into registers and other funky stuff, so it's easy. for
i386
architecture, you need to load a fixed reference address, like
load the stack pointer of a function to get the fixed address.
An example of this behaviour in 32 bit architecture.
0000040c <function>: 40c: 55 push %ebp 40d: 89 e5 mov %esp,%ebp 40f: e8 0e 00 00 00 call 422 <__i686.get_pc_thunk.cx> 414: 81 c1 5c 11 00 00 add $0x115c,%ecx 41a: 8b 81 18 00 00 00 mov 0x18(%ecx),%eax 420: 5d pop %ebp 421: c3 ret 00000422 <__i686.get_pc_thunk.cx>: 422: 8b 0c 24 mov (%esp),%ecx 425: c3 ret
In any case, you get the address of the functions this way. During runtime.
Now two cases arise when shared libraries are used:
- PIE on
In this case, relative offset to the PLT is used. The first invocation jumps to
puts@plt
where it jumps to another address in in.got.plt
. Over there in the first invocation it jumps back to.plt
to the immediate next address, which further goes to the runtime linker/resolver by the name of_dl_runtime_resolve
.In the next ISR/stack frame call/whatever you call it, the address at
.got.plt
is resolved and overwritten by the final resolved address for future invocation.Then in the future invocations of the same function it immediately jumps to the function directly instead of the resolution bullwork. This also decreases execution time btw.
- PIE off
Most of the details remain the same except the
.plt
address remains constant across invocations even if the library itself shifts because of ASLR.