Chapter 24 Key Takeaways: Dynamic Linking in Depth
-
The PLT (Procedure Linkage Table) provides a fixed call target for external functions. Each external function gets a 16-byte PLT stub with three instructions: an indirect jump through the GOT, a push of the relocation index, and a jump to the resolver stub (PLT[0]). Code always calls the PLT stub's fixed address — never the function's actual address directly.
-
The GOT (Global Offset Table) holds runtime addresses of external symbols. Initially, each GOT entry points back into the PLT (the
pushinstruction after thejmp). After the first call resolves the symbol, the GOT entry is updated to the actual function address. The GOT is in a writable memory segment to allow runtime updates. -
Lazy binding defers symbol resolution until first use. On the first call, the PLT → PLT[0] →
_dl_runtime_resolvepath runs, updates the GOT, and then jumps to the real function. On every subsequent call, the PLT stub's indirect jump goes directly to the real function — one extra memory access overhead. -
LD_BIND_NOW=1or the-z nowlinker flag disables lazy binding, resolving all symbols at program startup. This adds startup overhead but eliminates the_dl_runtime_resolvecall on the first use of each function. -
Full RELRO (
-Wl,-z,relro -Wl,-z,now) marks the GOT read-only after startup. By disabling lazy binding first (so all GOT entries are populated at startup), the entire GOT including.got.pltcan bemprotect'd to read-only. This prevents GOT overwrite attacks that redirect function calls to shellcode. -
LD_PRELOAD achieves function interposition by loading a library before all others. Symbol resolution uses the first matching definition; a function in an LD_PRELOAD library shadows the real library function. The real function is accessible via
dlsym(RTLD_NEXT, "function_name"). LD_PRELOAD is disabled for setuid/setgid programs for security. -
dlopen/dlsym/dlcloseenable runtime plugin systems.dlopenloads a shared library (or returns a handle to one already loaded) at any point during execution.dlsymfinds a named symbol in the loaded library.dlclosedecrements the reference count; the library is unloaded when the count reaches zero. This enables extensible architectures where plugins are.sofiles loaded on demand. -
RTLD_GLOBALvs.RTLD_LOCALindlopencontrols symbol visibility:RTLD_LOCAL(default) — symbols in the loaded library are only visible to that library.RTLD_GLOBAL— symbols are available for resolution by subsequently loaded libraries.RTLD_NEXTindlsymfinds the next occurrence of a symbol after the caller — the mechanism for calling through to the real implementation in an LD_PRELOAD wrapper. -
__attribute__((constructor))and__attribute__((destructor))run before and aftermainvia theDT_INIT_ARRAYandDT_FINI_ARRAYmechanism. LD_PRELOAD libraries' constructors run before the main program's constructors. Priority numbers (e.g.,__attribute__((constructor(200)))) control ordering within a binary — lower numbers run first for constructors, higher numbers run first for destructors. -
Symbol versioning (e.g.,
printf@@GLIBC_2.2.5) enables ABI stability across library updates. The same.sofile exports multiple versions of the same symbol. Programs record which version they linked against; the dynamic linker provides exactly that version at runtime. This is why Linux binaries remain binary-compatible across major distribution updates. -
**
$ORIGIN` in RPATH/RUNPATH expands to the directory of the executable (or library)** at runtime. This enables relocatable installations: the executable and its libraries can be moved to any directory as a group. Without `$ORIGIN, RPATH must be an absolute path, breaking if the installation directory changes. -
The
.dynamicsection is the dynamic linker's roadmap:DT_NEEDEDlists required libraries,DT_SYMTAB/DT_STRTABpoint to dynamic symbol/string tables,DT_JMPRELpoints to.rela.plt(lazy relocation entries),DT_INIT_ARRAY/DT_FINI_ARRAYpoint to constructor/destructor pointer arrays. Everything the dynamic linker needs to load and link a program is encoded here. -
GOT overwrite is a classic attack vector: A write-anywhere vulnerability (heap overflow, format string, use-after-free) can overwrite a GOT entry, redirecting the next call to that function to attacker-controlled code. The defense stack — Full RELRO + PIE + ASLR — addresses this: RELRO makes the GOT read-only, PIE randomizes all code addresses, and ASLR randomizes library base addresses. Modern Linux distributions enable all three by default.
-
ltraceand GDB'sx/xg addressare the essential tools for PLT/GOT analysis.ltraceintercepts library calls by patching GOT entries — the same mechanism as LD_PRELOAD.x/xgin GDB reads the raw GOT value before and after the first call, making the lazy binding state transition directly visible. Understanding these tools requires understanding the PLT/GOT mechanism; the mechanism becomes fully concrete through these tools.