It's a way to get code fixups (adjusting addresses based on where code sits in virtual memory, which may be different across different processes) without having to maintain a separate copy of the code for each process. The PLT is the procedure linkage table, one of the structures which makes dynamic loading and linking easier to use.
printf@plt
is actually a small stub which (eventually) calls the real printf
function, modifying things on the way to make subsequent calls faster.
The real printf
function may be mapped into any location in a given process (virtual address space) as may the code that is trying to call it.
So, in order to allow proper code sharing of calling code (left side below) and called code (right side below), you don't want to apply any fixups to the calling code directly since that will restrict where it can be located in other processes.
So the PLT
is a smaller process-specific area at a reliably-calculated-at-runtime address that isn't shared between processes, so any given process is free to change it however it wants to, without adverse effects.
Examine the following diagram which shows both your code and the library code mapped to different virtual addresses in two different processes, ProcA
and ProcB
:
Address: 0x1234 0x9000 0x8888
+-------------+ +---------+ +---------+
| | | Private | | |
ProcA | | | PLT/GOT | | |
| Shared | +---------+ | Shared |
========| application |=============| library |==
| code | +---------+ | code |
| | | Private | | |
ProcB | | | PLT/GOT | | |
+-------------+ +---------+ +---------+
Address: 0x2020 0x9000 0x6666
This particular example shows a simple case where the PLT maps to a fixed location. In your scenario, it's located relative to the current program counter as evidenced by your program-counter-relative lookup:
<printf@plt+0>: jmpq *0x2004c2(%rip) ; 0x600860 <_GOT_+24>
I've just used fixed addressing to keep the example simpler.
The original way in which code was shared meant it they had to be loaded at the same memory location in each virtual address space of every process that used it. Either that or it couldn't be shared, since the act of fixing up the single shared copy for one process would totally stuff up other processes where it was mapped to a different location.
By using position independent code, along with the PLT and a global offset table (GOT), the first call to a function printf@plt
(in the PLT) is a multi-stage operation, in which the following actions take place:
- You call
printf@plt
in the PLT.
- It calls the GOT version (via a pointer) which initially points back to some set-up code in the PLT.
- This set-up code loads the relevant shared library if not yet done, then modifies the GOT pointer so that subsequent calls directly to the real
printf
rather than the PLT set-up code.
- It then calls the loaded
printf
code at the correct address for this process.
On subsequent calls, because the GOT pointer has been modified, the multi-stage approach is simplified:
- You call
printf@plt
in the PLT.
- It calls the GOT version (via pointer), which now points to the real
printf
.
A good article can be found here, detailing how glibc
is loaded at run time.