A dynamically linked shared library may contain hundreds or thousands of functions, but the program might only use a few, so resolving all function addresses at load time may incur a high performance penalty, which is why function addresses are lazily binded through PLT by default. To see how PLT and GOT work together to lazily bind function addresses, we can compile a simple program that uses glibc:
We add a breakpoint at main, and we see that call puts@plt
is still a few instructions ahead, so the GOT should be unpopulated.
Let’s see how the PLT and GOT looks right now, manully for now; note that pwndbg does provide us with the commands plt
and got
which makes it easy to inspect. We are interested in these two sections: .plt
, which jumps to the .got.plt
entry, and .got.plt
, which is the GOT section responsible for function symbol addresses.
.plt
prior to the puts
call is illustrated below. We can see that puts@plt
jumps to the beginning of the .plt
section, which seems to jump elsewhere. We can also see that .plt.got
(5040) is placed immediately after the only entry in .plt
(5030).
rip
arithmeticI was originally looking at the disassembly for
.plt
and I was pretty confused as to howjmp QWORD PTR [rip+0x2fca]
reads the content at 8000 instead of 7ffa. Dumb me. It turns outrip
always points to the next instruction. So we would haverip+0x2fca
=0x55555555036+0x2fca
=0x555555558000
whose memory points toputs@got.plt
.
We can set a breakpoint on puts@plt
and see what actually happens:
Thanks to Unicorn Engine’s emulation, we can see that puts@plt
tries to jump to the address in the GOT (puts@got.plt
), but since the GOT entry isn’t populated yet, it contains a pointer back to the next instruction in puts@plt
which pushes an index of the function in PLT (in this case 0, since its the first and only function in .plt
), and jump to the top of .plt
to resolve the address of puts using _dl_runtime_resolve_xsavec
.
The next push instruction seems to push the base address of the loaded program, after which we jump to the ld’s realm to resolve puts
.
It turns out the xavec
suffix of _dl_runtime_resolve
stands for the xavec
instruction, which saves processor state to memory. I’m not exactly sure what role it plays here. Perhaps reading the source code would help.
A bit later, _dl_runtime_resolve_xsavec
kindly jumps to puts
for us after loading the address.
Now let’s compare .got.plt
before and after the address is resolved:
Instead of returning back to the PLT, the GOT now points to the actual puts function.