Shared Libraries & Dynamic Linking

Why This Matters

It is Monday morning and your application will not start. The error is cryptic:

error while loading shared libraries: libssl.so.3: cannot open shared object file:
No such file or directory

If you do not understand how shared libraries and dynamic linking work, this error is baffling. The file is right there on disk -- you can see it with ls. But the program cannot find it. Why?

Shared libraries are the invisible plumbing of every Linux system. When you run almost any program -- ls, curl, python, nginx -- it does not contain all the code it needs inside its own binary. Instead, it relies on shared libraries (.so files) that are loaded into memory at runtime. This saves enormous amounts of disk space and RAM, and it means a security fix to a library like OpenSSL instantly protects every program that uses it.

But this architecture also means that when a library is missing, moved, or the wrong version, programs break in confusing ways. This chapter gives you the complete picture: how shared libraries work, how the dynamic linker finds them, and how to diagnose and fix every common library problem you will encounter.


Try This Right Now

Pick any command on your system and see which shared libraries it depends on:

$ ldd /usr/bin/curl
	linux-vdso.so.1 (0x00007ffd5a7e6000)
	libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f3a1c800000)
	libz.so.1 => /usr/lib/x86_64-linux-gnu/libz.so.1 (0x00007f3a1c7e0000)
	libssl.so.3 => /usr/lib/x86_64-linux-gnu/libssl.so.3 (0x00007f3a1c730000)
	libcrypto.so.3 => /usr/lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f3a1c200000)
	libpthread.so.0 => /usr/lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f3a1c1f0000)
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f3a1c000000)
	...

That is curl depending on at least seven shared libraries. Every one of them must be present and findable at runtime.


Static vs Shared Libraries

There are two ways to package reusable code for programs to use:

Static Libraries (.a files)

A static library is an archive of compiled code that gets copied into the program at compile time (linking stage). The resulting binary contains everything it needs.

┌──────────────────────────────────────────────────────────┐
│  Compile Time                                             │
│                                                           │
│  program.c  +  libfoo.a  ──>  program (self-contained)   │
│                                                           │
│  The binary contains a copy of libfoo's code.             │
│  No external library needed at runtime.                   │
└──────────────────────────────────────────────────────────┘

Advantages:

  • No runtime dependencies -- the binary is self-contained
  • No "library not found" errors
  • Portable across systems with the same architecture

Disadvantages:

  • Larger binary size (every program has its own copy of the library code)
  • If a library has a bug fix, every program must be recompiled
  • More memory usage (each running program has its own copy in RAM)

Shared Libraries (.so files)

A shared library is compiled code that is loaded at runtime when the program starts. Multiple programs can share the same library in memory.

┌──────────────────────────────────────────────────────────┐
│  Compile Time                                             │
│                                                           │
│  program.c  ──>  program (contains reference to libfoo)   │
│                                                           │
│  Runtime                                                  │
│                                                           │
│  program starts                                           │
│       │                                                   │
│       ▼                                                   │
│  Dynamic linker (ld-linux.so) loads libfoo.so             │
│       │                                                   │
│       ▼                                                   │
│  program runs with libfoo's code available                │
│                                                           │
│  If another program also uses libfoo.so, the kernel       │
│  shares the same physical memory pages.                   │
└──────────────────────────────────────────────────────────┘

Advantages:

  • Smaller binaries
  • Update the library once, all programs benefit
  • Less memory usage (shared across processes via memory mapping)
  • Security patches are effective immediately

Disadvantages:

  • Runtime dependency -- the library must be present when the program runs
  • Version conflicts ("dependency hell")
  • Slightly slower startup (the dynamic linker must find and load libraries)

In practice, shared libraries are the default on Linux. Static linking is used in specific cases like Go binaries, containerized applications, and rescue utilities.

Think About It: When a critical security vulnerability is found in OpenSSL, what is the advantage of shared libraries? How many programs need to be updated?


Library Naming Conventions

Shared library names on Linux follow a specific pattern that encodes versioning information:

libfoo.so          Linker name     (symlink, used at compile time)
     │
     ▼
libfoo.so.1        SONAME          (symlink, used at runtime for ABI version)
     │
     ▼
libfoo.so.1.4.2    Real name       (actual file with full version)

Let us see this in practice:

$ ls -la /usr/lib/x86_64-linux-gnu/libssl*
lrwxrwxrwx 1 root root     13 ... libssl.so -> libssl.so.3
lrwxrwxrwx 1 root root     17 ... libssl.so.3 -> libssl.so.3.0.11
-rw-r--r-- 1 root root 684544 ... libssl.so.3.0.11

Three files (two symlinks and one real file):

  1. libssl.so.3.0.11 -- The real file. Contains the actual library code. Version 3.0.11.
  2. libssl.so.3 -- The SONAME (shared object name). A symlink to the real file. Programs record this name when compiled, so at runtime the dynamic linker looks for libssl.so.3. Any version 3.x.y can satisfy this.
  3. libssl.so -- The linker name. A symlink used only at compile time. When you compile with -lssl, the linker looks for libssl.so. This file is typically only installed with the -dev package.

Why Three Names?

This three-level system enables backward compatibility:

┌────────────────────────────────────────────────────┐
│  Your program was compiled against libssl.so.3      │
│                                                     │
│  libssl.so.3 -> libssl.so.3.0.11                   │
│                                                     │
│  The library gets a security update:                │
│  libssl.so.3 -> libssl.so.3.0.12   (NEW)           │
│                                                     │
│  Your program still works! It looks for             │
│  libssl.so.3, and the symlink was updated.          │
│  No recompilation needed.                           │
│                                                     │
│  But if the ABI changes (breaking change):          │
│  The new library becomes libssl.so.4                │
│  Your program still looks for libssl.so.3           │
│  Both can exist simultaneously.                     │
└────────────────────────────────────────────────────┘

You can check a library's SONAME:

$ objdump -p /usr/lib/x86_64-linux-gnu/libssl.so.3.0.11 | grep SONAME
  SONAME               libssl.so.3

How the Dynamic Linker Finds Libraries

When you run a program, the kernel loads the binary and then hands control to the dynamic linker (ld-linux-x86-64.so.2 on 64-bit systems). The dynamic linker's job is to find and load all required shared libraries.

The search order is:

┌──────────────────────────────────────────────────────────┐
│  Dynamic Linker Library Search Order                      │
│                                                           │
│  1. RPATH encoded in the binary (compile-time setting)    │
│  2. LD_LIBRARY_PATH environment variable                  │
│  3. RUNPATH encoded in the binary (compile-time setting)  │
│  4. /etc/ld.so.cache (precomputed lookup table)           │
│  5. Default paths: /lib, /usr/lib                         │
│                                                           │
│  The linker searches in this order and uses the FIRST     │
│  matching library it finds.                               │
└──────────────────────────────────────────────────────────┘

/etc/ld.so.conf and ldconfig

The file /etc/ld.so.conf (and files in /etc/ld.so.conf.d/) list additional directories where the dynamic linker should look for libraries:

$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf

$ ls /etc/ld.so.conf.d/
libc.conf
x86_64-linux-gnu.conf

$ cat /etc/ld.so.conf.d/x86_64-linux-gnu.conf
/usr/local/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu

After modifying these files, you must run ldconfig to rebuild the cache:

$ sudo ldconfig

ldconfig does three things:

  1. Scans the directories listed in /etc/ld.so.conf
  2. Creates the SONAME symlinks (e.g., libssl.so.3 -> libssl.so.3.0.11)
  3. Updates /etc/ld.so.cache (a binary file that the dynamic linker reads for fast lookups)

You can see what is in the cache:

$ ldconfig -p | head -20
1847 libs found in cache `/etc/ld.so.cache'
	libz.so.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libz.so.1
	libxml2.so.2 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libxml2.so.2
	...

# Search for a specific library
$ ldconfig -p | grep libssl
	libssl.so.3 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libssl.so.3

LD_LIBRARY_PATH

This environment variable adds directories to the library search path at runtime. It is the quick-and-dirty way to help a program find its libraries.

# Set it for a single command
$ LD_LIBRARY_PATH=/opt/myapp/lib ./myapp

# Or export it
$ export LD_LIBRARY_PATH=/opt/myapp/lib:$LD_LIBRARY_PATH
$ ./myapp

Safety Warning: Do NOT set LD_LIBRARY_PATH globally (in .bashrc or system-wide profile) as a permanent fix. It affects every program you run and can cause subtle breakage. Use it for testing only. For permanent solutions, add the directory to /etc/ld.so.conf.d/ and run ldconfig.

Safety Warning: Never set LD_LIBRARY_PATH for setuid programs. The dynamic linker ignores it for setuid binaries as a security measure, because allowing arbitrary library loading would enable privilege escalation.


The ldd Command

ldd shows the shared libraries required by a program and whether the dynamic linker can find them:

$ ldd /usr/bin/python3
	linux-vdso.so.1 (0x00007ffdb23fe000)
	libpython3.11.so.1.0 => /usr/lib/x86_64-linux-gnu/libpython3.11.so.1.0 (0x00007f...)
	libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6 (0x00007f...)
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
	...

Each line shows:

  • The library name the binary expects
  • => where it was found on disk
  • The memory address where it will be loaded

When a library is not found:

$ ldd ./myapp
	libcustom.so.1 => not found
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)

That not found is exactly what causes the "cannot open shared object file" error at runtime.

Safety Warning: Do not run ldd on untrusted binaries. On some systems, ldd may actually execute the binary to determine its dependencies. For untrusted binaries, use objdump -p binary | grep NEEDED instead.

Checking Libraries Without ldd

# Using objdump (safer for untrusted binaries)
$ objdump -p /usr/bin/curl | grep NEEDED
  NEEDED               libcurl.so.4
  NEEDED               libz.so.1
  NEEDED               libpthread.so.0
  NEEDED               libc.so.6

# Using readelf
$ readelf -d /usr/bin/curl | grep NEEDED
 0x0000000000000001 (NEEDED)   Shared library: [libcurl.so.4]
 0x0000000000000001 (NEEDED)   Shared library: [libz.so.1]
 0x0000000000000001 (NEEDED)   Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)   Shared library: [libc.so.6]

Troubleshooting "cannot open shared object file"

This is the most common library error. Let us walk through a systematic diagnosis.

The Error

$ ./myapp
./myapp: error while loading shared libraries: libfoo.so.2: cannot open
shared object file: No such file or directory

Step-by-Step Diagnosis

Step 1: Confirm which library is missing.

$ ldd ./myapp | grep "not found"
	libfoo.so.2 => not found

Step 2: Search for the library on disk.

# Is it installed anywhere?
$ find / -name "libfoo.so*" 2>/dev/null
/opt/custom/lib/libfoo.so.2.1.0
/opt/custom/lib/libfoo.so.2

The library exists, but in a non-standard location.

Step 3: Check if the path is in the linker cache.

$ ldconfig -p | grep libfoo
# (no output -- it's not in the cache)

Step 4: Fix it.

Option A -- Add the path to the linker configuration (recommended for permanent fix):

$ echo "/opt/custom/lib" | sudo tee /etc/ld.so.conf.d/custom.conf
$ sudo ldconfig

# Verify
$ ldconfig -p | grep libfoo
	libfoo.so.2 (libc6,x86-64) => /opt/custom/lib/libfoo.so.2

Option B -- Use LD_LIBRARY_PATH (for testing):

$ LD_LIBRARY_PATH=/opt/custom/lib ./myapp

Option C -- The library is genuinely not installed. Find and install the package that provides it:

# Debian/Ubuntu
$ apt-file search libfoo.so.2
libfoo2: /usr/lib/x86_64-linux-gnu/libfoo.so.2

$ sudo apt install libfoo2

# Fedora/RHEL
$ dnf provides */libfoo.so.2

Step 5: Handle version mismatches.

Sometimes the library exists but with a different version:

$ ls /usr/lib/x86_64-linux-gnu/libfoo*
libfoo.so.3
libfoo.so.3.1.0

The program wants libfoo.so.2 but only version 3 is installed. This means:

  • The program was compiled against an older version of the library
  • Version 3 has breaking ABI changes (hence the different SONAME)
  • You need to either: install the older library version alongside the new one, or recompile the program against the new version

Think About It: Why can libfoo.so.2 and libfoo.so.3 coexist on the same system? What does the number after .so. represent?


Using strace for Library Debugging

When ldd does not give you enough information, strace shows you exactly what the dynamic linker is doing at the system call level.

$ strace -e openat ./myapp 2>&1 | grep "\.so"
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libfoo.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libfoo.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT
openat(AT_FDCWD, "/usr/lib/libfoo.so.2", O_RDONLY|O_CLOEXEC) = -1 ENOENT

This reveals exactly which directories the linker searched and that every attempt returned ENOENT (file not found).

You can also use the LD_DEBUG environment variable for detailed linker diagnostics:

$ LD_DEBUG=libs ./myapp 2>&1 | head -40
     12345:	find library=libfoo.so.2 [0]; searching
     12345:	 search cache=/etc/ld.so.cache
     12345:	 search path=/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu:...
     12345:	  trying file=/usr/lib/x86_64-linux-gnu/libfoo.so.2
     12345:	  trying file=/lib/x86_64-linux-gnu/libfoo.so.2
     12345:
     12345:	./myapp: error while loading shared libraries: libfoo.so.2:
     cannot open shared object file: No such file or directory

Other useful LD_DEBUG values:

$ LD_DEBUG=files ./myapp     # Show file operations
$ LD_DEBUG=bindings ./myapp  # Show symbol binding
$ LD_DEBUG=versions ./myapp  # Show version dependencies
$ LD_DEBUG=all ./myapp       # Show everything (very verbose)
$ LD_DEBUG=help ./myapp      # List all available debug options

pkg-config: Finding Library Information

pkg-config is a helper tool that provides the compiler and linker flags needed to use a library. It reads .pc files installed by library development packages.

# What compiler flags does openssl need?
$ pkg-config --cflags openssl
-I/usr/include/openssl

# What linker flags?
$ pkg-config --libs openssl
-lssl -lcrypto

# What version is installed?
$ pkg-config --modversion openssl
3.0.11

# Does a specific version satisfy a requirement?
$ pkg-config --exists "openssl >= 3.0" && echo "yes" || echo "no"
yes

This is how ./configure scripts typically detect libraries:

# In a configure script, this is essentially what happens:
PKG_CHECK_MODULES([OPENSSL], [openssl >= 1.1])
# Translates to: pkg-config --exists "openssl >= 1.1"

The .pc files live in standard locations:

$ pkg-config --variable pc_path pkg-config
/usr/local/lib/x86_64-linux-gnu/pkgconfig:/usr/local/lib/pkgconfig:...

$ cat /usr/lib/x86_64-linux-gnu/pkgconfig/openssl.pc
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib/x86_64-linux-gnu
includedir=${prefix}/include

Name: OpenSSL
Description: Secure Sockets Layer and cryptography libraries and tools
Version: 3.0.11
Requires: libssl libcrypto

If pkg-config cannot find a library you know is installed, you may need to set PKG_CONFIG_PATH:

$ export PKG_CONFIG_PATH=/opt/custom/lib/pkgconfig:$PKG_CONFIG_PATH
$ pkg-config --libs customlib

RPATH and RUNPATH

Sometimes a program needs to carry its own library search path inside its binary. This is done with RPATH or RUNPATH.

What Are They?

Both RPATH and RUNPATH are paths stored inside the ELF binary that tell the dynamic linker where to search for libraries. The key difference is search order:

With RPATH:     RPATH → LD_LIBRARY_PATH → /etc/ld.so.cache → defaults
With RUNPATH:   LD_LIBRARY_PATH → RUNPATH → /etc/ld.so.cache → defaults

RPATH takes precedence over LD_LIBRARY_PATH. RUNPATH does not. Modern toolchains prefer RUNPATH because it is more flexible (you can still override with LD_LIBRARY_PATH).

Checking RPATH/RUNPATH

$ readelf -d /usr/bin/someapp | grep -E "RPATH|RUNPATH"
 0x000000000000001d (RUNPATH)     Library runpath: [/opt/myapp/lib]

# Or with objdump
$ objdump -p /usr/bin/someapp | grep -E "RPATH|RUNPATH"
  RUNPATH              /opt/myapp/lib

Setting RPATH/RUNPATH at Compile Time

# Using gcc directly
$ gcc -o myapp myapp.c -lfoo -Wl,-rpath,/opt/myapp/lib

# Using CMake
$ cmake .. -DCMAKE_INSTALL_RPATH=/opt/myapp/lib

# Using autotools
$ ./configure LDFLAGS="-Wl,-rpath,/opt/myapp/lib"

Modifying RPATH After Compilation

The patchelf tool can modify the RPATH/RUNPATH of an existing binary:

$ sudo apt install patchelf    # or dnf/pacman equivalent

# View current RPATH
$ patchelf --print-rpath ./myapp
/opt/old/lib

# Set a new RPATH
$ patchelf --set-rpath /opt/new/lib ./myapp

# Remove RPATH entirely
$ patchelf --remove-rpath ./myapp

The $ORIGIN Variable

RPATH supports a special variable $ORIGIN that resolves to the directory containing the executable. This is useful for self-contained application bundles:

/opt/myapp/
├── bin/
│   └── myapp         (RUNPATH = $ORIGIN/../lib)
└── lib/
    └── libfoo.so.2
$ gcc -o myapp myapp.c -lfoo -Wl,-rpath,'$ORIGIN/../lib'

No matter where /opt/myapp is installed, the binary will find its libraries relative to its own location.


Hands-On: Library Exploration Lab

Exercise 1: Map a program's complete library dependencies

# Start with a complex program
$ ldd /usr/bin/python3 | wc -l
12

# Now recursively find ALL libraries (libraries that depend on libraries)
$ ldd /usr/bin/python3
	linux-vdso.so.1 (0x00007ffd...)
	libpython3.11.so.1.0 => /usr/lib/x86_64-linux-gnu/libpython3.11.so.1.0
	libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6
	libexpat.so.1 => /usr/lib/x86_64-linux-gnu/libexpat.so.1
	libz.so.1 => /usr/lib/x86_64-linux-gnu/libz.so.1
	...

# Check what libpython itself depends on
$ ldd /usr/lib/x86_64-linux-gnu/libpython3.11.so.1.0
	libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6
	libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6
	libpthread.so.0 => ...

Exercise 2: Create and use a shared library

# Step 1: Write a simple library
$ cat > mylib.c << 'EOF'
#include <stdio.h>

void greet(const char *name) {
    printf("Hello, %s! Greetings from a shared library.\n", name);
}
EOF

# Step 2: Write a header file
$ cat > mylib.h << 'EOF'
void greet(const char *name);
EOF

# Step 3: Write a program that uses it
$ cat > main.c << 'EOF'
#include "mylib.h"

int main() {
    greet("Linux learner");
    return 0;
}
EOF

# Step 4: Compile the shared library
$ gcc -shared -fPIC -o libmylib.so.1.0.0 mylib.c
$ ln -sf libmylib.so.1.0.0 libmylib.so.1      # SONAME symlink
$ ln -sf libmylib.so.1 libmylib.so             # Linker name symlink

# Step 5: Compile the program
$ gcc -o myapp main.c -L. -lmylib -Wl,-rpath,.

# Step 6: Run it
$ ./myapp
Hello, Linux learner! Greetings from a shared library.

# Step 7: Verify the library dependency
$ ldd ./myapp | grep mylib
	libmylib.so.1 => ./libmylib.so.1 (0x00007f...)

# Step 8: What happens without the library?
$ mv libmylib.so.1.0.0 /tmp/
$ ./myapp
./myapp: error while loading shared libraries: libmylib.so.1:
cannot open shared object file: No such file or directory

# Step 9: Restore it
$ mv /tmp/libmylib.so.1.0.0 .
$ ./myapp
Hello, Linux learner! Greetings from a shared library.

Exercise 3: The linker cache in action

# Step 1: Install your library system-wide
$ sudo cp libmylib.so.1.0.0 /usr/local/lib/
$ sudo ln -sf libmylib.so.1.0.0 /usr/local/lib/libmylib.so.1
$ sudo ln -sf libmylib.so.1 /usr/local/lib/libmylib.so

# Step 2: Update the cache
$ sudo ldconfig

# Step 3: Verify it is in the cache
$ ldconfig -p | grep mylib
	libmylib.so.1 (libc6,x86-64) => /usr/local/lib/libmylib.so.1

# Step 4: Recompile without -rpath and it still works
$ gcc -o myapp main.c -L/usr/local/lib -lmylib
$ ldd ./myapp | grep mylib
	libmylib.so.1 => /usr/local/lib/libmylib.so.1 (0x00007f...)
$ ./myapp
Hello, Linux learner! Greetings from a shared library.

# Step 5: Clean up
$ sudo rm /usr/local/lib/libmylib.so*
$ sudo ldconfig

Debug This

A developer deploys an application to a production server. The application works perfectly on their development machine but fails on the server:

$ /opt/myapp/bin/server
/opt/myapp/bin/server: error while loading shared libraries: libboost_system.so.1.74.0:
cannot open shared object file: No such file or directory

They check:

$ ldconfig -p | grep libboost_system
	libboost_system.so.1.83.0 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libboost_system.so.1.83.0

The library exists, but version 1.83.0 is installed. The binary wants version 1.74.0.

What happened and how would you fix it?

Diagnosis: The binary was compiled on a system with Boost 1.74 and hard-coded the SONAME libboost_system.so.1.74.0. The production server has Boost 1.83, which has a different SONAME (the major version number in the SONAME changed, indicating an ABI break).

Options:

  1. Install the matching library version alongside the newer one (if the package exists):

    $ sudo apt install libboost-system1.74.0
    
  2. Recompile the application on the production server (or a system with matching libraries).

  3. Create a symlink (risky -- only if the ABI is actually compatible):

    # DANGEROUS -- only as a last resort after testing
    $ sudo ln -s libboost_system.so.1.83.0 /usr/lib/x86_64-linux-gnu/libboost_system.so.1.74.0
    $ sudo ldconfig
    
  4. Ship the libraries with the application and set RUNPATH:

    $ patchelf --set-rpath '/opt/myapp/lib' /opt/myapp/bin/server
    # Then copy the correct libraries to /opt/myapp/lib/
    

Option 4 is the most robust for deployment -- it makes the application self-contained.


What Just Happened?

┌──────────────────────────────────────────────────────────────┐
│                                                               │
│  In this chapter, you learned:                                │
│                                                               │
│  - Static libraries (.a) are linked at compile time.          │
│    Shared libraries (.so) are loaded at runtime.              │
│                                                               │
│  - Library naming: libfoo.so (linker name) ->                 │
│    libfoo.so.1 (SONAME) -> libfoo.so.1.2.3 (real file).      │
│    The SONAME enables backward-compatible updates.            │
│                                                               │
│  - The dynamic linker searches: RPATH -> LD_LIBRARY_PATH ->   │
│    RUNPATH -> /etc/ld.so.cache -> default paths.              │
│                                                               │
│  - ldd shows a binary's library dependencies.                 │
│    Use objdump -p for untrusted binaries.                     │
│                                                               │
│  - /etc/ld.so.conf.d/ configures library search paths.        │
│    Run ldconfig after changes to rebuild the cache.           │
│                                                               │
│  - "cannot open shared object file" means the dynamic         │
│    linker cannot find a required library. Fix by installing    │
│    the library, adding its path to ld.so.conf, or setting     │
│    LD_LIBRARY_PATH (for testing only).                        │
│                                                               │
│  - strace and LD_DEBUG reveal exactly what the linker          │
│    is searching for and where.                                │
│                                                               │
│  - pkg-config provides compiler/linker flags for libraries.   │
│                                                               │
│  - RPATH/RUNPATH embed library search paths in binaries.      │
│    patchelf can modify them after compilation.                │
│                                                               │
└──────────────────────────────────────────────────────────────┘

Try This

Exercises

  1. Library census: Run ldd on five different programs (curl, python3, bash, vim, ssh). Which shared libraries appear in all of them? What does that tell you about those libraries?

  2. Create a shared library: Follow the hands-on exercise above to create libmylib.so, but add a second version (libmylib.so.2.0.0) with a different function signature. Install both versions and verify they can coexist.

  3. Break and fix: Compile a program that depends on a shared library. Then move the library to a non-standard location and observe the error. Fix it three different ways: (a) LD_LIBRARY_PATH, (b) /etc/ld.so.conf.d/, (c) patchelf --set-rpath.

  4. strace investigation: Run strace -e openat /usr/bin/curl --version 2>&1 | grep .so and trace every library the dynamic linker opens. How many files does it try before finding each library?

  5. pkg-config audit: Run pkg-config --list-all to see every library with pkg-config support on your system. Pick three and examine their .pc files.

Bonus Challenge

Write a script that checks all binaries in /usr/local/bin/ for missing library dependencies. For each binary, run ldd and report any libraries marked as not found. This is a useful health check after system upgrades.


What's Next

Now that you understand shared libraries and how the dynamic linker works, you have the foundation to understand something much larger: the Linux kernel itself. Chapter 60 covers how to upgrade your kernel, what DKMS does for third-party modules, and how to build a custom kernel from source -- the ultimate compilation project.