The Dynamic Linker (ld.so)

20 minadvancedModule 7: Dynamic LinkingContinues from shared-libraries

Why This Matters

Every dynamically-linked program on Linux starts with ld.so, not main(). The dynamic linker loads all shared libraries, processes relocations, and only then hands control to your program. Understanding ld.so explains "library not found" errors, how LD_PRELOAD works, and why some programs crash at startup.

A Simple Model

ld.so is the first thing that runs when you start a dynamically-linked program:

  1. The kernel maps the program into memory and jumps to ld.so's entry point (stored in the PT_INTERP segment)
  2. ld.so maps all DT_NEEDED shared libraries into memory
  3. ld.so processes .rela.dyn (data relocations) and .rela.plt (lazy binding setup)
  4. ld.so calls init functions (DT_INIT, .init_array)
  5. ld.so jumps to the program's entry point (_start)

The Actual Detail

The library search order (in order of priority):

DT_RPATH: embedded in the binary (deprecated, but still works)

LD_LIBRARY_PATH: environment variable (colon-separated list)

DT_RUNPATH: embedded in the binary (preferred over DT_RPATH)

/etc/ld.so.cache: pre-built cache from ldconfig (fast lookup)

/lib and /usr/lib: default paths

Key environment variables:

VariablePurposeExample
LD_LIBRARY_PATHAdditional library search pathsLD_LIBRARY_PATH=/opt/foo/lib ./myprog
LD_PRELOADForce-load libraries before all othersLD_PRELOAD=libmalloc.so ./myprog
LD_BIND_NOWDisable lazy binding (resolve all symbols at load)LD_BIND_NOW=1 ./myprog
LD_DEBUGPrint linker debug infoLD_DEBUG=libs ./myprog
LD_TRACE_LOADED_OBJECTSList dependencies and exit (like ldd)LD_TRACE_LOADED_OBJECTS=1 ./myprog

A Real Example

# Watch ld.so search for libraries
$ LD_DEBUG=libs ./myprog | head -20
     12345:     find library=libc.so.6 [0]; searching
     12345:      search cache=/etc/ld.so.cache
     12345:       trying file=/lib/x86_64-linux-gnu/libc.so.6
     12345:     init: /lib/x86_64-linux-gnu/libc.so.6
     12345:     find library=libfoo.so.1 [0]; searching
     12345:      search cache=/etc/ld.so.cache
     12345:       trying file=/usr/lib/x86_64-linux-gnu/libfoo.so.1
     12345:     init: /usr/lib/x86_64-linux-gnu/libfoo.so.1

# See what ldd actually does
$ ldd /bin/ls
    linux-vdso.so.1 (0x00007ffd...)
    libselinux.so.1 --- /lib/x86_64-linux-gnu/libselinux.so.1
    librt.so.1 --- /lib/x86_64-linux-gnu/librt.so.1
    libc.so.6 --- /lib/x86_64-linux-gnu/libc.so.6

The LD_DEBUG output shows the exact search sequence — incredibly useful for debugging "library not found" errors.

Try It

# Debug library loading
LD_DEBUG=libs ls | head -30

# Trace symbol resolution
LD_DEBUG=bindings ls | head -20

# Force a library to be loaded first (interposition)
LD_PRELOAD=/lib/x86_64-linux-gnu/libc.so.6 ls

# Check what a binary needs without running it
ldd /bin/ls

# Find which package provides a library
dpkg -S /lib/x86_64-linux-gnu/libc.so.6

Check Your Understanding

What is the purpose of LD_PRELOAD?

Apply It

You get "error while loading shared libraries: libfoo.so.3: cannot open shared object file". What are the most likely causes?

Connect

You now understand the full dynamic linking pipeline. The final module covers how the OS loader actually runs an ELF program — the kernel-side counterpart to ld.so.

Order the Steps

Put the dynamic linker loading steps in the correct order (drag to reorder):

3Process DT_NEEDED entries and load each required shared library
1Kernel maps ELF into memory and passes control to ld.so
4Perform relocations (patch GOT/PLT with resolved addresses)
2Parse dynamic section of the executable and libraries
5Jump to the entry point (e_entry) to start program execution