This is a way to get code fixes (adjusting addresses depending on the location of the code in virtual memory, which may vary in different processes) without having to maintain a separate copy of the code for each process. PLT is a procedure binding table, one of the structures that simplifies the use of dynamic loading and binding.
printf@plt is actually a small stub that (in the end) calls the real printf function, changing the way you speed up subsequent calls.
The actual printf function can be mapped anywhere in the process (virtual address space), just like the code that tries to call it.
Thus, in order to allow the correct use of the code of the calling code (bottom left) and the called code (bottom right), you do not want to apply any corrections directly to the calling code, as this will limit its location in other processes.
Thus, PLT is a smaller process-specific area, with a reliably calculated address at runtime that is not shared between processes, so any particular process can change it as it sees fit, without negative consequences.
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 specific example shows a simple case where the PLT is mapped to a fixed location. In your scenario, it is located relative to the current program counter, as evidenced by your search for the program counter:
<printf@plt+0>: jmpq *0x2004c2(%rip) ; 0x600860 <_GOT_+24>
I just used fixed addressing to simplify the example.
The original way of sharing code meant that they had to be loaded into the same memory area in each virtual address space of each process that used it. Either this, or it cannot be divided, since the process of repairing one common copy for one process completely fills other processes in which it was mapped to a different location.
Using position-independent code along with PLT and the Global Offset Table (GOT), the first call to printf@plt (in PLT) is a multi-step operation in which the following actions are performed:
- You call
printf@plt in PLT. - It calls the GOT version (via a pointer), which initially points to some setup code in the PLT.
- This installation code loads the appropriate shared library, if not already done, then modifies the GOT pointer so that subsequent calls directly to the real
printf and not to the PLT installation code. - It then calls the downloaded
printf code at the correct address for this process.
In subsequent calls, since the GOT pointer has been changed, the multi-step approach is simplified:
- You call
printf@plt in PLT. - It calls the GOT version (via a pointer), which now points to the actual
printf .
A good article can be found here detailing how glibc loads at runtime.