Most binaries are linked dynamically to reduce file size. Unlike static binaries, dynamic binaries do not include their library dependencies as part of the executable, but instead rely on the host system to provide the necessary libraries.
To further improve performance, symbol lookup (resolution of function names to the actual code in memory) in dynamic binaries is done lazily. In lazy binding, the symbol lookup is only carried out the first time the function is called and stored for later use. This way, library functions that are never used by the binary are never loaded or resolved.
The mechanism behind lazy binding mainly relies on two program sections: the Global Offset Table (GOT) and the Procedure Linkage Table (PLT). There is also a third section, got.plt, that stores references to addresses in the PLT.
The lazy binding process can be visualized with GDB. In the below examples, the GOT and got.plt are listed before and then after the callme_one function has been called.
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /home/admin/sboxshare/ropemporium/03-callme/callme:
GOT protection: Partial RELRO | Found 9 GOT entries passing the filter
[0x601018] puts@GLIBC_2.2.5 -> 0x7f374806aaa0 (puts) ◂— endbr64
[0x601020] printf@GLIBC_2.2.5 -> 0x7f3748042b40 (printf) ◂— endbr64
[0x601028] callme_three -> 0x4006f6 (callme_three@plt+6) ◂— push 2
[0x601030] memset@GLIBC_2.2.5 -> 0x7f3748155480 (__memset_avx2_unaligned_erms) ◂— endbr64
[0x601038] read@GLIBC_2.2.5 -> 0x7f37480f4980 (read) ◂— endbr64
[0x601040] callme_one -> 0x400726 (callme_one@plt+6) ◂— push 5
[0x601048] setvbuf@GLIBC_2.2.5 -> 0x7f374806b250 (setvbuf) ◂— endbr64
[0x601050] callme_two -> 0x400746 (callme_two@plt+6) ◂— push 7
[0x601058] exit@GLIBC_2.2.5 -> 0x400756 (exit@plt+6) ◂— push 8
pwndbg> disass 'callme_one@got.plt'
Dump of assembler code for function callme_one@got.plt:
0x0000000000400720 <+0>: jmp QWORD PTR [rip+0x20091a] # 0x601040 <callme_one@got.plt>
0x0000000000400726 <+6>: push 0x5
0x000000000040072b <+11>: jmp 0x4006c0
End of assembler dump.
Before functions are resolved, their entries in the PLT are short stubs like the one above. The stubs push a reference to the the stack 0x5 in the case above and jump back to the top of the PLT. The entire PLT can be disassembled from the target address on the last line:
pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)
State of the GOT of /home/admin/sboxshare/ropemporium/03-callme/callme:
GOT protection: Partial RELRO | Found 9 GOT entries passing the filter
[0x601018] puts@GLIBC_2.2.5 -> 0x7f374806aaa0 (puts) ◂— endbr64
[0x601020] printf@GLIBC_2.2.5 -> 0x7f3748042b40 (printf) ◂— endbr64
[0x601028] callme_three -> 0x4006f6 (callme_three@plt+6) ◂— push 2
[0x601030] memset@GLIBC_2.2.5 -> 0x7f3748155480 (__memset_avx2_unaligned_erms) ◂— endbr64
[0x601038] read@GLIBC_2.2.5 -> 0x7f37480f4980 (read) ◂— endbr64
[0x601040] callme_one -> 0x7f374820081a (callme_one) ◂— push rbp
[0x601048] setvbuf@GLIBC_2.2.5 -> 0x7f374806b250 (setvbuf) ◂— endbr64
[0x601050] callme_two -> 0x400746 (callme_two@plt+6) ◂— push 7
[0x601058] exit@GLIBC_2.2.5 -> 0x400756 (exit@plt+6) ◂— push 8
Once the callme_one function is called and resolved, the address on the GOT is updated to point to the callme_one function code. The code can be found by disassembling from the highlighted address above, or by repeating the disassembly from got.plt from earlier: