Shared Libraries¶
Most Linux programs are dynamically linked: at start-up the loader (ld.so) finds each shared library the binary names and maps it into memory. When that search fails, the program never runs, which makes the search order the first thing to know for error while loading shared libraries.
Track: Advanced · Interview weight: Med
Must-Know Facts¶
| Fact | Value | Verify with |
|---|---|---|
| Loader | /lib64/ld-linux-x86-64.so.2, named in the ELF header | file <binary> |
| Needed libraries | NEEDED entries in the dynamic section | readelf -d <binary> |
| Resolved paths | ldd shows where each library is found | ldd <binary> |
| Search order | LD_LIBRARY_PATH, the binary's RUNPATH, /etc/ld.so.cache, then the default directories; a legacy RPATH is searched before LD_LIBRARY_PATH | LD_DEBUG=libs <binary> |
| Cache | ldconfig builds /etc/ld.so.cache from /etc/ld.so.conf.d/*.conf | ldconfig -p |
| soname | Name a library promises to stay compatible under (libgreet.so.1) | readelf -d lib.so |
| Symlinks | libx.so.1.0 real file; libx.so.1 for programs (soname); libx.so for the compiler (-dev or -devel packages) | ls -l |
RUNPATH | Directories stored in the binary; $ORIGIN means the binary's own directory | patchelf --set-rpath |
LD_PRELOAD | Loads a library first, overriding symbols; ignored for SUID programs | LD_PRELOAD=... cmd |
| Symbol versions | GLIBC_2.34 in a binary sets the minimum glibc | objdump -T <binary> |
| Static binaries | Contain their libraries; ldd prints not a dynamic executable | ldd <binary> |
Building a Library and a Program¶
A one-function library, built with a soname and the usual symlinks, and a program linked against it:
gcc -shared -fPIC -Wl,-soname,libgreet.so.1 -o libgreet.so.1.0 greet.c
ln -sf libgreet.so.1.0 libgreet.so.1
ln -sf libgreet.so.1 libgreet.so
gcc -o app app.c -L. -lgreet
ls -l libgreet*
readelf -d app | grep -E 'NEEDED|RUNPATH|RPATH'
readelf -d libgreet.so.1.0 | grep SONAME
Output:
lrwxrwxrwx 1 laborant laborant 13 Sep 16 18:36 libgreet.so -> libgreet.so.1
lrwxrwxrwx 1 laborant laborant 15 Sep 16 18:36 libgreet.so.1 -> libgreet.so.1.0
-rwxrwxr-x 1 laborant laborant 15544 Sep 16 18:36 libgreet.so.1.0
0x0000000000000001 (NEEDED) Shared library: [libgreet.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libgreet.so.1]
The linker used libgreet.so to build, but recorded the soname libgreet.so.1, which is what the loader looks for at run time.
When the Library Is Not Found¶
./app; echo "rc=$?"
ldd ./app | grep greet
LD_LIBRARY_PATH=$PWD ./app
Output:
./app: error while loading shared libraries: libgreet.so.1: cannot open shared object file: No such file or directory
rc=127
libgreet.so.1 => not found
hello, world
The current directory is not searched. There are three permanent fixes:
| Fix | How | Suits |
|---|---|---|
| Register the directory | File in /etc/ld.so.conf.d/, then ldconfig | Libraries shared by several programs |
| Store the path in the binary | RUNPATH at link time (-Wl,-rpath,...) or patchelf --set-rpath | Self-contained application bundles |
| Wrapper script | LD_LIBRARY_PATH=... exec app "$@" | Vendor software that cannot be rebuilt |
sudo mkdir -p /opt/greet/lib
sudo cp -a libgreet.so* /opt/greet/lib/
echo /opt/greet/lib | sudo tee /etc/ld.so.conf.d/greet.conf
sudo ldconfig
ldconfig -p | grep greet
./app
ldd ./app | grep greet
Output:
/opt/greet/lib
libgreet.so.1 (libc6,x86-64) => /opt/greet/lib/libgreet.so.1
libgreet.so (libc6,x86-64) => /opt/greet/lib/libgreet.so
hello, world
libgreet.so.1 => /opt/greet/lib/libgreet.so.1 (0x00007feee4ef3000)
A new library directory needs ldconfig
Adding a .conf file changes nothing until ldconfig rebuilds /etc/ld.so.cache. Package managers run it in their scripts; manual installs into /opt or /usr/local/lib often forget it.
With the configuration removed again, RUNPATH and $ORIGIN make a relocatable bundle:
patchelf --set-rpath '$ORIGIN/../lib' app
readelf -d app | grep RUNPATH
mkdir -p ../bundle/bin ../bundle/lib && cp app ../bundle/bin/ && cp -a libgreet.so* ../bundle/lib/
../bundle/bin/app
Output:
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
hello, world
Watching the Search¶
LD_DEBUG=libs ../bundle/bin/app 2>&1 | grep -E 'find library|trying file' | head -4
/lib64/ld-linux-x86-64.so.2 --list ../bundle/bin/app | head -3
cat /etc/ld.so.conf; ls /etc/ld.so.conf.d/
Output:
6738: find library=libgreet.so.1 [0]; searching
6738: trying file=/home/laborant/bundle/bin/../lib/glibc-hwcaps/x86-64-v4/libgreet.so.1
6738: trying file=/home/laborant/bundle/bin/../lib/glibc-hwcaps/x86-64-v3/libgreet.so.1
6738: trying file=/home/laborant/bundle/bin/../lib/glibc-hwcaps/x86-64-v2/libgreet.so.1
linux-vdso.so.1 (0x00007ffc3b290000)
libgreet.so.1 => /home/laborant/libdemo/../bundle/bin/../lib/libgreet.so.1 (0x00007f1889a9d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1889800000)
include /etc/ld.so.conf.d/*.conf
libc.conf
x86_64-linux-gnu.conf
The loader tries CPU-specific subdirectories (glibc-hwcaps/x86-64-v4 and lower) inside each search directory before the directory itself.
ldd can run code from the binary
ldd may execute the program's loader, so running it on an untrusted binary is unsafe. readelf -d or objdump -p read the same information without executing anything.
Preloading and Symbol Versions¶
LD_PRELOAD loads a library before all others, so its symbols win:
gcc -shared -fPIC -o libshout.so shout.c
LD_PRELOAD=$PWD/libshout.so ../bundle/bin/app
nm -D --defined-only libgreet.so.1.0 | grep greet
objdump -T ../bundle/bin/app | grep -E 'greet|GLIBC' | head -3
Output:
HELLO, world! (preloaded)
0000000000001119 T greet
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000 DF *UND* 0000000000000000 Base greet
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
LD_PRELOAD is how memory debuggers and libfaketime work, and also a known persistence trick; /etc/ld.so.preload applies it system-wide and is worth checking during an incident. The GLIBC_2.34 reference means this binary needs glibc 2.34 or newer.
Common Errors¶
error while loading shared libraries: libgreet.so.1: cannot open shared object file: No such file or directory¶
Cause: the library is not installed, or its directory is not in the search path (exit status 127).
Fix: ldd <binary> to see what is missing; install the package (dnf provides '*/libgreet.so.1', apt-file search libgreet.so.1) or register the directory with ldconfig.
A binary built on Fedora 44 (glibc 2.43) that calls pthread_gettid_np(), added in glibc 2.42, copied to Ubuntu 24.04:
./app-fedora44; echo "rc=$?"
ldd --version | head -1
objdump -T app-fedora44 | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1
Output:
./app-fedora44: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.42' not found (required by ./app-fedora44)
rc=1
ldd (Ubuntu GLIBC 2.39-0ubuntu8.9) 2.39
GLIBC_2.42
version `GLIBC_2.42' not found (required by ./app-fedora44)¶
Cause: the binary was built against a newer glibc than the one installed.
Fix: build on the oldest supported target, link statically, or run it in a container based on the build distribution.
Interview Checkpoints¶
L1: What is the difference between static and dynamic linking?
Say first: static linking copies library code into the binary; dynamic linking records library names that the loader finds and maps at start-up, so libraries are shared and updated separately.
Proof: ldd /usr/bin/ssh lists libraries; a static Go binary reports not a dynamic executable.
Follow-up: What is the security consequence of each for patching?
L2: A vendor tool fails with error while loading shared libraries. Find and fix the missing library.
Say first: list the missing names, find the package or directory, then register it.
Proof: ldd ./tool | grep 'not found'; install the package, or echo /opt/vendor/lib | sudo tee /etc/ld.so.conf.d/vendor.conf && sudo ldconfig.
Follow-up: Why not export LD_LIBRARY_PATH in /etc/profile?
L2: Show which glibc version a binary needs.
Say first: read the versioned symbol references.
Proof: objdump -T ./app | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1
Follow-up: How do you check the installed glibc? (ldd --version.)
L2: Ship an application with its own libraries in one directory.
Say first: set a relative RUNPATH with $ORIGIN.
Proof: patchelf --set-rpath '$ORIGIN/../lib' bin/app, or link with -Wl,-rpath,'$ORIGIN/../lib'.
Follow-up: How does that compare with a container image?
L3: An application works when started from a shell but fails under systemd with a shared library error.
Say first: compare environments: the shell has LD_LIBRARY_PATH, the unit does not.
Proof: echo $LD_LIBRARY_PATH in the shell; /proc/<pid>/environ of the service lacks it.
Follow-up: What is the better fix than Environment=LD_LIBRARY_PATH=?
L3: During an incident, every command on a server behaves oddly, including ls.
Say first: check for a system-wide preload.
Proof: cat /etc/ld.so.preload lists an unknown library; ldd /usr/bin/ls shows it loaded first.
Follow-up: Why can a statically linked busybox help here?
L4: What happens between execve and main for a dynamically linked program?
Say first: the kernel maps the program and its PT_INTERP loader and starts the loader, which reads NEEDED entries, searches and maps each library, resolves relocations, runs initializers, then jumps to the program's entry point.
Proof: LD_DEBUG=libs,reloc ./app and strace -e trace=openat,mmap ./app show the searches and mappings before any program output.
Don't say: "The kernel loads the shared libraries."
Related¶
- Architecture: the C library and the loader
- Packaging Concepts: soname dependencies such as
libc.so.6()(64bit) - Command Resolution:
required file not foundfor a missing loader - Round 4: Internals: more "what happens when" questions
Captured on Rocky Linux 10.2, Fedora 44 (where noted) and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.