Symbol Resolution On Linux
This post discusses how a compiled C program (Elf) on Linux resolves the location in memory for shared libraries and includes a walkthrough of the Procedure Linkage Table (PLT) and Global Offset Table (GOT).
Procedure Linkage Table (PLT)
The Procedure Linkage Table (PLT) is a read-only section responsible for calling the dynamic linker during and after program runtime to resolve addresses of requested functions.
Global Offset Table
At runtime, the dynamic linker populates the Global Offset Table (GOT). The dynamic linker obtains the absolute addresses of requested functions for locations from the PLT. This is not always handled during compile time.
Lazy binding and the PLT
Symbol resolution for many functions is performed when the first call to the requested function is made. (Lazy Linking) You can force the linker to perform all relocations immediately at runtime for performance reasons by setting the environmental variable “LD_BIND_NOW”.
ELF binaries contain a seperate GOT section named “.got.plt”. This section is similar to the regular .got section. The difference is the .got.plt is runtime-writable but .got is not if RELRO is enabled. RELRO is relocations read-only. To enable it, use the ld option -z relro
. RELRO places GOT entries that must be runtime-writable for lazy binding in .got.plt
and all others in the read-only .got
section. [1]
If you would like to read more about the PLT, GOT, and security features, see page 45 of “Practical Binary Analysis”. [1]
High level overview of symbol resolution
- The program makes a call to printf()
- The request is made to the PLT which refers to the GOT entry for the requested symbol.
- If this is the first request for the function, control is passed by the GOT back to the PLT. This is done by first pushing the address for the relative entry within the relocation table, which is used by the dynamic linker for symbol resolution. The PLT then calls the dynamic linker to resolve the symbol Upon successful resolution, the address of the requested function is placed in the GOT entry.
- If the GOT holds the address of the requested function, control can be passed immediately without the involvement of the dynamic linker.
Commands:
Objdump
objdump -d
disassembles an object file
objdump -h
displays section headers
objdump -j <section name>
allows you to specify a section, example: objdump -d .text -d ./program_name
objdump -R ./program_name
Lists the Global Offset Table (GOT) for each function.
Readelf
readelf -S [program name]
Display the sections’ header
readelf -x [section name/number] [program name]
Hex dump a section name, example: readelf -x [section to display] [program name]
Example: walking through dynamic relocation
(gdb) disas main
Dump of assembler code for function main:
<snip>
0x080484a4 <+83>: call 0x8048374 <puts@plt>
<snip>
$ objdump -d -j .text memtest | grep puts
80484a4: e8 cb fe ff ff call 8048374 <puts@plt>
Since we haven’t called the puts function at this point, it has not been resolved.
Now we will use objdump to dump the GOT:
$ objdump -R [program name]
program_name: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
080497cc R_386_GLOB_DAT __gmon_start__
080497dc R_386_JUMP_SLOT getpid@GLIBC_2.0
080497e0 R_386_JUMP_SLOT __gmon_start__
080497e4 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
080497e8 R_386_JUMP_SLOT scanf@GLIBC_2.0
080497ec R_386_JUMP_SLOT printf@GLIBC_2.0
080497f0 R_386_JUMP_SLOT puts@GLIBC_2.0
We notice above that address 0x8048374 is no longer shown. It shows address 0x080497f0 for puts function. Lets examine three instructions from that address in gdb:
(gdb) x/3i 0x8048374
0x8048374 <puts@plt>: jmp *0x80497f0
0x804837a <puts@plt+6>: push $0x28
0x804837f <puts@plt+11>: jmp 0x8048314
Notice above the jmp to address 0x80497f0.
Let’s examine in objdump:
$ objdump -j .got.plt -d [program name] | grep 80497f0
80497f0: 7a 83 04 08
The “7a 83 04 08” is in little-endian format, so we reverse it to get address 0x804837a.
We see that address above, which shows that by default, the GOT entry points back to the PLT. Now the dynamic linker will be called so that it can write the real address of the function into it’s GOT location.
Open memtest again in gdb, enter run to run it, enter any number and press enter, then enter Ctrl-C. Now enter the following in GDB:
(gdb) x/x 0x80497f0
0x80497f0 <puts@got.plt>: 0xf7e453d0
Now we have the real address of the puts function:
(gdb) x/3i 0xf7e453d0
0xf7e453d0 <puts>: push ebp
0xf7e453d1 <puts+1>: mov ebp,esp
0xf7e453d3 <puts+3>: push edi
References
[1] Andriesse, Dennis. Practical Binary Analysis. San Francisco. No Starch Press, 2019