On this page

Dynamic Linking and ld.so

Coverage: PLT/GOT → lazy binding → LD_PRELOAD → LD_LIBRARY_PATH → rpath/runpath → symbol versioning → dlopen/dlsym → IFUNC Applicable to: glibc ld.so, musl ldso

Overview

Dynamic linking allows multiple programs to share the same code page of a .so (only one copy in physical memory), while also allowing libraries to be updated independently (without relinking the programs). The cost is a slight delay on the first call to each external function (lazy binding) and the burden of maintaining ABI compatibility. Understanding the PLT/GOT mechanism is a prerequisite for troubleshooting issues such as "undefined symbol", "LD_PRELOAD not taking effect", and "libfoo.so.1 vs libfoo.so.2".

PLT/GOT: The Engine of Lazy Binding

The Problem with Static Linking

// main.c:
extern int foo(int);
int main() { return foo(42); }

// Static linking: The linker hardcodes the address of foo in the call instruction
//   call <address_of_foo>
//   Problem: Every time libfoo.so is updated → the address of foo changes → all callers must be relinked

Dynamic Linking: PLT + GOT

flowchart TD
    MAIN["Program (main)<br/>call foo@PLT<br/>Does not call foo directly, but calls the PLT stub"]

    MAIN --> PLT{"PLT[foo]<br/>jmp *GOT[foo]"}

    PLT -->|"First call<br/>GOT → next PLT instruction"| LAZY["push index<br/>Pushes the index of foo in .rela.plt"]

    PLT -->|"Subsequent calls<br/>GOT → real address of foo"| DIRECT["jmp foo<br/>Direct jump ✅<br/><br/>Each call adds only one indirect jump"]

    LAZY --> RESOLVER["jmp PLT[0]<br/>→ _dl_runtime_resolve()"]

    RESOLVER --> FIND["🔍 Look up the address of function foo in libfoo.so"]

    FIND --> UPDATE["📝 Update GOT[foo]<br/>= real address of foo"]

    UPDATE --> EXEC["▶️ jmp foo<br/>Execute the real foo"]

    classDef entry fill:#e3f2fd,stroke:#1565c0
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef resolver fill:#f3e5f5,stroke:#7b1fa2
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class MAIN entry
    class PLT decision
    class LAZY,RESOLVER,FIND,UPDATE resolver
    class DIRECT,EXEC done

Three-Part Memory Layout

.got.plt (Global Offset Table — PLT specific):
   GOT[0]:  Address of the .dynamic section
   GOT[1]:  struct link_map * (link map of the current shared library)
   GOT[2]:  Address of dl_runtime_resolve (filled by ld.so)
   GOT[3+]: Addresses of various external functions (initially pointing to the second instruction of PLT[func])

.plt (Procedure Linkage Table):
   PLT[0]:  Common entry — push link_map + jmp dl_runtime_resolve
   PLT[1+]: Stubs for each function — jmp *GOT[n]; push index; jmp PLT[0]

.got (Global data for non-PLT):
   Stores addresses of global variables (e.g., extern int errno;)

Practical Test

# Compile and view PLT/GOT:
gcc -o test main.c -lfoo
objdump -d test | grep -A3 'foo@plt'
objdump -R test | grep foo      # View the .rela.plt entry for foo

# Confirm lazy binding at runtime:
LD_DEBUG=bindings ./test 2>&1 | grep foo
# Output: binding file ./test [0] to ./test [0]: normal symbol `foo'

ld.so: The Dynamic Linker

Loading and Searching

1. Kernel loads ELF → sees PT_INTERP segment → /lib64/ld-linux-x86-64.so.2
2. Kernel loads ld.so into memory → jumps to ld.so's _start
3. ld.so bootstraps (relocates itself, because ld.so is also dynamically linked!)
4. ld.so reads the main program's .dynamic → DT_NEEDED → loads dependent .so files
5. For each .so: reads .dynamic → loads dependencies of dependencies
6. Symbol resolution: Iterates through all loaded .so files → resolves all undefined symbols
7. Relocation: Fills GOT, handles R_X86_64_GLOB_DAT / R_X86_64_JUMP_SLOT
8. Calls the main program's _start

Search Path Order

1. DT_RPATH (RPATH in ELF .dynamic, deprecated, replaced by DT_RUNPATH)
2. LD_LIBRARY_PATH (user override)
3. DT_RUNPATH (RUNPATH in ELF .dynamic)
4. /etc/ld.so.cache (generated by ldconfig)
5. /lib64, /usr/lib64 (default paths)

Checks:
  readelf -d /bin/ls | grep -E 'RPATH|RUNPATH|NEEDED'
  ldconfig -p | grep libfoo
  LD_DEBUG=libs ./test 2>&1  # Trace all searches

LD_PRELOAD

# Force insertion of a shared library at the very beginning of symbol resolution → override any symbol
LD_PRELOAD=./override.so ./program

# Typical uses:
#   1. Replace malloc → track memory allocations
#   2. Replace connect → network redirection
#   3. Replace open → filesystem sandbox

# Conditions: Cannot be used to override statically linked functions
#       (Static linking has no PLT/GOT, call goes directly to the function address)

dlopen / dlsym

#include <dlfcn.h>
void *handle = dlopen("libfoo.so", RTLD_LAZY | RTLD_LOCAL);
void (*func)(int) = dlsym(handle, "foo");
func(42);
dlclose(handle);

// RTLD_LAZY:   Lazy binding (default)
// RTLD_NOW:    Resolve all symbols immediately (dlopen fails earlier if issues exist)
// RTLD_GLOBAL: Symbols from this library are visible to subsequent dlopen calls
// RTLD_LOCAL:  Symbols are visible only to this handle (default)
// RTLD_NODELETE: dlclose does not unload (prevents dangling pointers)
// RTLD_NOLOAD:  Do not load, only check if already loaded

Symbol Versioning and ABI

// Symbol versioning (GNU extension):
//   libfoo.so: foo@@VERS_2.0 (default), foo@VERS_1.0 (old)
//   At link time: Take the default version
//   At runtime: Check if the loaded .so has a matching version

// GCC's -fno-semantic-interposition (5.x+):
//   Tells the compiler that function calls within the library can assume "this library's version"
//   → Skips PLT → Direct call → Faster
//   But disables the effect of LD_PRELOAD on this function

Debugging

# View all linked shared libraries
ldd /bin/ls

# View symbol resolution (extremely detailed)
LD_DEBUG=all ./test 2>&1 | head -50

# View .so files loaded by the current process
cat /proc/self/maps | grep '\.so'

# Undefined symbols (at link time)
nm -u /bin/ls

References

  • Source code: glibc elf/dl-runtime.c (_dl_runtime_resolve), elf/rtld.c (ld.so main)
  • man pages: ld.so(8), dlopen(3), dlsym(3), elf(5)
  • LWN: "How to write shared libraries", "The cost of lazy binding"

Keywords: PLT, GOT, lazy binding, dl_runtime_resolve, LD_PRELOAD, dlopen, dlsym, DT_NEEDED, RPATH, RUNPATH, symbol versioning