Free — no signup required

The Loader: ld.so

2 min read

The Stage Manager

When you run a dynamically linked program, the kernel does not jump directly to your main(). It first hands control to the dynamic linker/loader, a special program embedded in the ELF file's interpreter field. On Linux x86-64, this is typically /lib64/ld-linux-x86-64.so.2.

You can see which interpreter an ELF file specifies — it's recorded in the ELF header's interpreter field, and ELF-inspection tools can print it directly. For most binaries on a modern x86-64 system it reads /lib64/ld-linux-x86-64.so.2.

The Loader's Sequence:

  1. Read the ELF header — identify required shared libraries (the NEEDED entries in the dynamic section).
  2. Search for each library — check, in order:
  3. LD_LIBRARY_PATH environment variable (if set)
  4. The binary's RPATH/RUNPATH (paths baked in at link time)
  5. /etc/ld.so.cache (a pre-built index of system libraries, updated by ldconfig)
  6. Default paths: /lib, /usr/lib, /lib64, /usr/lib64
  7. Map each library into memory — using mmap() syscalls.
  8. Resolve symbols (relocation) — fill in the blank addresses left by the compiler. Connect each call to printf to the actual address of printf in the loaded libc.so.
  9. Run initializers — execute any __attribute__((constructor)) functions in libraries.
  10. Jump to main() — finally, your code runs.

The error error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory means the loader failed at step 2. The fix is to either install the library, add its path to LD_LIBRARY_PATH, or — if it was placed in a standard directory — refresh /etc/ld.so.cache with ldconfig, which rebuilds that pre-built index so the loader picks up newly installed libraries without a reboot.

Lazy vs. Eager Binding

By default, the loader uses lazy binding: symbol addresses are resolved only when a function is called for the first time, not at startup. This speeds up program launch. The PLT (Procedure Linkage Table) and GOT (Global Offset Table) are the ELF mechanisms that implement lazy binding — a first call goes through a stub that resolves the address and patches the GOT, so subsequent calls go directly to the function.

You can force eager binding — resolving every symbol before main() runs, rather than on first use — by setting the LD_BIND_NOW environment variable. This is useful for security hardening (combined with read-only relocations, RELRO, which makes the resolved GOT immutable afterward) and for catching missing symbols at startup rather than discovering them mid-execution, potentially in production, when some rarely-used code path finally runs.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free