Managing Shared Libraries on Linux: ldconfig, ldd, and LD_LIBRARY_PATH Explained

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

The Problem: Your App Compiled Fine, But Won’t Run

You build a C++ application, everything compiles clean, then you run it:

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

The library is right there in /usr/local/lib. You can see it. Yet Linux refuses to find it — which feels like the system is lying.

It’s not. Linux doesn’t crawl the entire filesystem for shared libraries at runtime. It reads a pre-built cache. When that cache doesn’t include your directory, the dynamic linker fails even if the file is sitting three folders away. Once that clicks, the fix goes from mysterious to mechanical.

How Linux Resolves Shared Libraries: Three Approaches

Three mechanisms control where Linux looks for shared libraries. They differ in scope, persistence, and the situations where each one belongs.

Approach 1: System-Wide Registration with ldconfig

ldconfig reads /etc/ld.so.conf and the drop-in files under /etc/ld.so.conf.d/, scans every listed directory, then writes a binary cache to /etc/ld.so.cache. At startup, the dynamic linker (ld.so) reads that cache directly — no filesystem traversal, just a fast lookup.

Default search paths are /lib, /lib64, /usr/lib, and /usr/lib64. Anything outside those directories needs explicit registration.

Approach 2: LD_LIBRARY_PATH Environment Variable

LD_LIBRARY_PATH prepends extra directories to the library search path for the current process and its children. It overrides the ldconfig cache entirely.

export LD_LIBRARY_PATH=/usr/local/lib/myapp:$LD_LIBRARY_PATH
./myapp

No cache rebuild. No config files. No root access. Changes take effect immediately in the current shell.

Approach 3: Embedding the Path at Link Time (rpath)

When compiling, you can bake a search path into the binary itself with -Wl,-rpath:

gcc -o myapp main.c -L/usr/local/lib -lfoo -Wl,-rpath,/usr/local/lib

That binary will always check that directory first, regardless of ldconfig or LD_LIBRARY_PATH. No external configuration required after deployment.

Pros and Cons of Each Approach

Approach Pros Cons
ldconfig System-wide, persistent, clean, all processes benefit Requires root, needs rebuild after adding new libraries
LD_LIBRARY_PATH Instant, no root needed, great for testing Session-only, security risk if set globally, silently overrides system libs
rpath Self-contained binary, works without any system config Hard to change after compile, breaks if the library moves

The security risk with LD_LIBRARY_PATH is worth taking seriously. A malicious library dropped into that path can hijack any program running in that session. That’s why the kernel ignores it entirely for setuid binaries. Never add it to /etc/environment or system-wide login scripts.

Recommended Setup for Most Cases

Libraries going into /usr/local, or anything installed via package manager? Use ldconfig. Clean, persistent, available to every process on the system.

Experimenting with a library version or debugging a build? LD_LIBRARY_PATH works well — but only for the current terminal session. Don’t let it leak into production init scripts.

Shipping a standalone application bundle or container image? rpath earns its place here. The binary carries its own path information and doesn’t depend on whatever ldconfig state exists on the target host.

On a Ubuntu 22.04 production server I manage, mixing these strategies — ldconfig for system libraries, rpath for bundled tools — cut deployment friction noticeably. A new engineer can provision the same machine and the libraries resolve without any manual fiddling.

Implementation Guide

Step 1: Diagnose the Problem with ldd

Start here every time. ldd lists every shared library a binary needs and shows whether the linker can find each one:

ldd /usr/bin/myapp

Sample output:

linux-vdso.so.1 (0x00007ffd3e9f7000)
libfoo.so.2 => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b2a200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8b2a600000)

That not found line is your target. Everything else is already resolved. Now find where the file actually lives:

find /usr -name "libfoo.so*" 2>/dev/null
find /usr/local -name "libfoo.so*" 2>/dev/null

Step 2: Register a New Library Directory with ldconfig

Compiled a library from source and it landed in /usr/local/lib? That path often isn’t in the default ldconfig list. Add it with one command:

echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/local-libs.conf

Rebuild the cache:

sudo ldconfig

Verify it worked:

ldconfig -p | grep libfoo
# Expected output:
# libfoo.so.2 (libc6,x86-64) => /usr/local/lib/libfoo.so.2

Run ldd again. The not found entry should be gone.

Step 3: Handle Missing Symlinks

Sometimes the library file exists but its symlinks are broken or absent. Shared libraries use a three-name convention:

  • libfoo.so.2.1.3 — the actual versioned file
  • libfoo.so.2 — soname symlink (what programs link against at runtime)
  • libfoo.so — linker name (used at compile time only)

If only the versioned file is present, create the symlinks manually:

cd /usr/local/lib
sudo ln -sf libfoo.so.2.1.3 libfoo.so.2
sudo ln -sf libfoo.so.2 libfoo.so
sudo ldconfig

Step 4: Use LD_LIBRARY_PATH for Testing

Need to test a specific library version without touching system config? Set the variable in your current shell:

# Temporary, current session only
export LD_LIBRARY_PATH=/home/user/myproject/libs:$LD_LIBRARY_PATH
./myapp

# Or inline for a single run
LD_LIBRARY_PATH=/home/user/myproject/libs ./myapp

When multiple versions of the same library exist, it’s not always obvious which one the linker picks. Use LD_DEBUG to see exactly what’s happening:

LD_DEBUG=libs ./myapp 2>&1 | grep libfoo

Step 5: Verify After Package Manager Installs

Package managers like apt and dnf run ldconfig automatically in their post-install scripts. Manual installs from tarballs don’t. After any manual library install, run it yourself:

# After any manual library install
sudo ldconfig

# Confirm
ldconfig -p | grep "your-library-name"

Bonus: Check What a Running Process Is Using

No restart needed to inspect a live process. Read directly from /proc:

# Get the PID
pgrep myapp

# List all loaded libraries for that process
cat /proc/<PID>/maps | grep "\.so"

# Or with lsof
lsof -p <PID> | grep ".so"

Quick Reference: The Decision Tree

  1. Run ldd ./binary — identify what’s missing.
  2. Run find /usr /usr/local -name "libXXX.so*" — check if the file exists.
  3. File exists but linker can’t find it: add the directory to /etc/ld.so.conf.d/ and run sudo ldconfig.
  4. Symlinks are missing: create them manually, then run sudo ldconfig.
  5. Library genuinely not installed: grab the dev package (apt install libfoo-dev on Debian/Ubuntu, dnf install libfoo-devel on RHEL/Fedora).
  6. Testing only: use LD_LIBRARY_PATH for the current session, nowhere else.

Shared library errors look cryptic until you understand one thing: Linux uses a cache, not a filesystem scan. Once that mental model is in place, the fix is almost always one of the five steps above. Start with ldd every time — it shows exactly what the linker sees and cuts straight through the guesswork.

Share: