A dynamic library is not just a collection of code; it’s a promise that the system can find and load that code on demand, and ld.so is the bouncer making sure only legitimate code gets in.

Let’s see it in action. Imagine we have a simple C program that uses a custom dynamic library.

mylib.c:

#include <stdio.h>

void hello_from_lib() {
    printf("Hello from the dynamic library!\n");
}

main.c:

extern void hello_from_lib();

int main() {
    hello_from_lib();
    return 0;
}

First, we compile the library into a shared object:

gcc -shared -fPIC -o libmylib.so mylib.c

-fPIC generates Position-Independent Code, essential for shared libraries. -shared tells GCC to create a shared library.

Next, we compile our main program, linking against the library:

gcc -o main main.c -L. -lmylib

-L. tells the compiler to look for libraries in the current directory. -lmylib instructs it to find libmylib.so.

Now, if we try to run main, we’ll likely get an error:

./main
./main: error while loading shared libraries: libmylib.so: cannot open shared object file: No such file or directory

This is ld.so’s way of saying it can’t find libmylib.so. By default, ld.so looks in a few standard locations (/lib, /usr/lib, etc.) and directories specified in /etc/ld.so.conf. Our libmylib.so is in the current directory, which isn’t one of those.

To fix this, we need to tell ld.so where to find our library. The most straightforward way for development is to use the LD_LIBRARY_PATH environment variable:

LD_LIBRARY_PATH=. ./main

And voilà:

Hello from the dynamic library!

LD_LIBRARY_PATH=. temporarily adds the current directory (.) to the list of places ld.so searches. This is great for testing and development but generally discouraged for production systems because it can lead to unintended library loading.

For a more permanent solution, you’d typically install the library in a system-wide location (like /usr/local/lib) and then run sudo ldconfig. This command updates the cache that ld.so uses, making your new library discoverable without LD_LIBRARY_PATH.

The system’s dynamic linker, ld.so (or ld-linux.so on Linux), is a crucial piece of the puzzle. When you execute a program that depends on shared libraries, ld.so steps in before your program’s main function even starts. Its job is to locate all the required shared libraries, load them into memory, and perform symbol resolution. Symbol resolution means ensuring that all the function calls and global variables your program uses from those libraries actually exist and are mapped to the correct memory addresses. If ld.so can’t find a library or a symbol within it, it halts execution and reports an error, just like we saw.

The search path for ld.so is determined by a hierarchy:

  1. LD_LIBRARY_PATH environment variable: Highest precedence for dynamic loading.
  2. ldconfig cache: This is a file, usually /etc/ld.so.cache, created by the ldconfig utility. ldconfig scans directories listed in /etc/ld.so.conf and files in /etc/ld.so.conf.d/ to build this cache of available libraries.
  3. Default library paths: Standard directories like /lib, /usr/lib, /lib64, /usr/lib64.

The ldconfig command is vital for managing the ld.so.cache. When you install new libraries or move them to standard locations, running sudo ldconfig rebuilds this cache, making the new libraries visible to ld.so for subsequent program executions. This avoids the need to constantly set LD_LIBRARY_PATH.

The readelf command is your best friend for understanding what’s inside a shared library or an executable that uses them. For instance, readelf -d libmylib.so | grep NEEDED will show you the dynamic dependencies of libmylib.so. If libmylib.so itself used other libraries, they would appear here. Similarly, readelf -d main | grep NEEDED will show you that main needs libmylib.so.

If your program is linked statically, ld.so isn’t involved at all because all the code is already part of the executable. Dynamic linking offers advantages like reduced memory footprint (multiple programs can share a single copy of a library in RAM) and easier updates (you can update a library without recompiling all programs that use it), but it introduces the complexity of library discovery and versioning.

The DT_NEEDED entries in an ELF executable’s dynamic section are what ld.so reads to know which libraries to load. When ld.so loads a library, it also processes that library’s DT_NEEDED entries, recursively loading its dependencies. This chain of dependencies is how complex applications are assembled from many smaller shared components.

One subtle point is how ld.so handles different architectures. If you try to run an x86_64 executable on an ARM machine, even if the libraries are present, ld.so will refuse to load them because they are for the wrong architecture. The file command can tell you an executable’s architecture (e.g., file main will show ELF 64-bit LSB executable, x86-64,...).

The next hurdle you’ll likely encounter is symbol versioning, where ld.so needs to pick the correct version of a library when multiple are available.

Want structured learning?

Take the full Linux & Systems Programming course →