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:

#include <stdio.h>
int main(int argc, char* argv[]) {
    puts("hello");
    return 0;
}

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).

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.