lazy binding
A large program might be linked against shared libraries that, between them, export thousands of functions — but on a typical run it calls only a small fraction of them. Resolving every one of those addresses at startup would slow the program's launch for functions it may never use. Lazy binding is the optimization that defers each function's address lookup until the moment of its first actual call, so you pay only for what you use, and you pay it spread out rather than all at once.
Here is the mechanism walked through. Each external function gets a PLT stub and a GOT slot. Initially the dynamic linker sets each GOT slot to point back into a special PLT entry that calls the resolver. The first time you call, say, foo(), the PLT stub jumps through foo's GOT slot, which lands in that resolver code; the resolver looks foo up in the loaded libraries, writes foo's real address into the GOT slot, and jumps to foo. The crucial part is the side effect: the GOT slot now holds foo's real address, so the SECOND and all later calls to foo go straight through with no resolver — the cost is paid exactly once, on first use. This is why it is called lazy: work is postponed until it is unavoidable.
It matters for startup time in big programs, but it comes with real caveats. The first call to each function is slightly slower because of the one-time resolution, which can perturb timing-sensitive code. More importantly, lazy binding requires the .got.plt to stay writable so the resolver can patch it, which weakens hardening. For this reason security-conscious builds often turn lazy binding OFF — either via full RELRO at build time or by setting the environment variable LD_BIND_NOW=1 — so every symbol is resolved eagerly at startup and the GOT can be locked read-only. So lazy binding is a speed-versus-security trade-off, not an unconditional win.
# First call to foo(): PLT stub -> GOT slot -> resolver writes real addr. # Every later call: PLT stub -> GOT slot already holds foo -> jump. $ LD_BIND_NOW=1 ./app # disable laziness: resolve everything at startup
The first call resolves and caches the address in the GOT; later calls are direct. LD_BIND_NOW disables laziness for hardening.
Lazy binding keeps .got.plt writable, which is a hardening weakness; full RELRO or LD_BIND_NOW resolves everything eagerly so the GOT can be read-only. The first call to each lazily-bound function is slightly slower than later calls.