Chapter 24 Key Takeaways: Dynamic Linking in Depth

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

  2. The GOT (Global Offset Table) holds runtime addresses of external symbols. Initially, each GOT entry points back into the PLT (the push instruction after the jmp). 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.

  3. Lazy binding defers symbol resolution until first use. On the first call, the PLT → PLT[0] → _dl_runtime_resolve path 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.

  4. LD_BIND_NOW=1 or the -z now linker flag disables lazy binding, resolving all symbols at program startup. This adds startup overhead but eliminates the _dl_runtime_resolve call on the first use of each function.

  5. 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.plt can be mprotect'd to read-only. This prevents GOT overwrite attacks that redirect function calls to shellcode.

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

  7. dlopen/dlsym/dlclose enable runtime plugin systems. dlopen loads a shared library (or returns a handle to one already loaded) at any point during execution. dlsym finds a named symbol in the loaded library. dlclose decrements the reference count; the library is unloaded when the count reaches zero. This enables extensible architectures where plugins are .so files loaded on demand.

  8. RTLD_GLOBAL vs. RTLD_LOCAL in dlopen controls 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_NEXT in dlsym finds the next occurrence of a symbol after the caller — the mechanism for calling through to the real implementation in an LD_PRELOAD wrapper.

  9. __attribute__((constructor)) and __attribute__((destructor)) run before and after main via the DT_INIT_ARRAY and DT_FINI_ARRAY mechanism. 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.

  10. Symbol versioning (e.g., printf@@GLIBC_2.2.5) enables ABI stability across library updates. The same .so file 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.

  11. **$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.

  12. The .dynamic section is the dynamic linker's roadmap: DT_NEEDED lists required libraries, DT_SYMTAB/DT_STRTAB point to dynamic symbol/string tables, DT_JMPREL points to .rela.plt (lazy relocation entries), DT_INIT_ARRAY/DT_FINI_ARRAY point to constructor/destructor pointer arrays. Everything the dynamic linker needs to load and link a program is encoded here.

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

  14. ltrace and GDB's x/xg address are the essential tools for PLT/GOT analysis. ltrace intercepts library calls by patching GOT entries — the same mechanism as LD_PRELOAD. x/xg in 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.