Static vs Dynamic Linking Explained
A dynamically linked program does not contain the library code it uses. It contains a list of libraries it needs, and the dynamic linker loads them at startup.
ldd /usr/bin/curl
linux-vdso.so.1 (0x00007ffd8f5f4000)
libcurl.so.4 => /lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f2a1c4a0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2a1c200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2a1c7e0000)
That last entry is the dynamic linker itself, and it is the program the kernel actually starts. It maps the required libraries into memory, resolves symbols, and only then transfers control to main.
Our ldd guide covers reading that output in more depth.
Why dynamic is the default
Memory. One copy of libc in physical memory, shared by every process on the system. With several hundred processes running, that is a substantial saving.
Security updates. A vulnerability in OpenSSL is fixed by updating one package. Every dynamically linked program picks up the fix on next start. If every program had statically linked it, each would need rebuilding and redistributing, and the ones nobody maintains would stay vulnerable forever.
Disk. Binaries are far smaller.
That second point is the strongest argument and is worth weighting heavily for anything that touches a network.
How libraries are found
The search order at startup:
DT_RPATHin the binary, if set andRUNPATHis absentLD_LIBRARY_PATHfrom the environmentDT_RUNPATHin the binary- The cache in
/etc/ld.so.cache /liband/usr/lib
ldconfig -p | grep libssl # what the cache knows
sudo ldconfig # rebuild after adding a library
cat /etc/ld.so.conf.d/*.conf # extra directories
After dropping a .so into /usr/local/lib, run ldconfig. Without it the cache does not know about the file and the linker will not find it, which produces a missing-library error for a library that is plainly there.
LD_LIBRARY_PATH is for testing:
LD_LIBRARY_PATH=/opt/mylib ./myprog
Setting it permanently in a shell profile is a mistake. It applies to everything launched from that shell, and a stray old library in that directory gets loaded by unrelated programs, producing failures that are extremely hard to attribute.
For a program that genuinely needs its own libraries, set RUNPATH at link time instead:
gcc -Wl,-rpath,'$ORIGIN/../lib' -o myprog myprog.c
$ORIGIN expands to the directory containing the binary, so the program finds its libraries relative to itself wherever it is installed. Quote it to stop the shell expanding it.
Versioning, and why it mostly works
ls -l /lib/x86_64-linux-gnu/libcurl.so*
# libcurl.so.4 -> libcurl.so.4.8.0
The 4 is the soname, the ABI version. Binaries link against libcurl.so.4, and any libcurl.so.4.x.y satisfies it. When the ABI changes incompatibly, the soname increments to .5, and both can coexist so old binaries keep working.
This is why library upgrades usually do not break anything, and why the occasional soname bump requires everything that links it to be rebuilt.
objdump -p /usr/bin/curl | grep NEEDED
readelf -d /usr/bin/curl | grep SONAME
Static linking
gcc -static -o myprog myprog.c
ldd ./myprog
# not a dynamic executable
One file, no runtime dependencies, runs anywhere with a compatible kernel.
The glibc problem
gcc -static -o myprog myprog.c
# /usr/bin/ld: warning: Using 'getaddrinfo' in statically linked
# applications requires at runtime the shared libraries from the
# glibc version used for linking
Read that warning. It is describing a real failure.
glibc’s name service switch loads modules at runtime based on /etc/nsswitch.conf. Hostname resolution, user lookups, and group lookups all go through it, and it uses dlopen to load libnss_files.so, libnss_dns.so, and friends.
A statically linked glibc binary still needs those shared objects at runtime, and it needs them from the same glibc version it was built against. So your “static” binary works on the build machine and fails on a different distribution with a confusing error from getaddrinfo, despite ldd reporting no dependencies.
musl, which actually works
sudo apt install musl-tools
musl-gcc -static -o myprog myprog.c
musl is a small libc designed for static linking and it has no NSS problem. A musl static binary is genuinely portable.
Go produces static binaries by default using its own runtime, which is a large part of why Go became popular for command-line tools. Rust can target x86_64-unknown-linux-musl for the same effect:
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
When static is right
Distributing one file to systems you do not control.
Container images. A static binary in a scratch image produces a container of a few megabytes with no distribution inside it and essentially no attack surface. This is the main reason static linking has become popular again.
FROM scratch
COPY myprog /myprog
ENTRYPOINT ["/myprog"]
Rescue tools. A statically linked busybox or shell works when the filesystem holding /lib is unmountable, which is exactly when you need it. Our chroot rescue guide covers that situation.
Reproducibility. No dependence on what the target system happens to have.
The cost remains: a library vulnerability requires rebuilding and redistributing every binary that contains it, and you must know which ones those are.
Diagnosing problems
# what is missing
ldd ./myprog | grep "not found"
# what is actually loaded at runtime
cat /proc/$(pgrep myprog)/maps | grep '\.so'
# trace the search
LD_DEBUG=libs ./myprog 2>&1 | head -40
# which package provides it
apt-file search libssl.so.3 # Debian, Ubuntu
dnf provides '*/libssl.so.3' # Fedora, RHEL
LD_DEBUG=libs prints every directory the linker tried, which tells you immediately whether the problem is a missing file or a path it never looked in.
Never run ldd on a binary you do not trust. On some systems it works by executing the binary with a special linker mode, which means a malicious binary can run code. objdump -p reads the file without executing anything and is the safe alternative.
Frequently Asked Questions
What is the difference between static and dynamic linking?
Static linking copies the library code into the executable at build time, producing one self-contained file. Dynamic linking records only a reference, and the dynamic linker loads the shared library into memory at program startup. Dynamic is the default on Linux because it saves memory and allows libraries to be patched independently.
Why do I get an error about a missing .so file?
The dynamic linker could not find a shared library the binary requires. Run ldd on the binary to see which one is missing, then install the package providing it or add its directory to the linker search path with ldconfig. A missing library is a startup failure, not a runtime one.
Should I statically link my program?
Usually not. Dynamic linking means security fixes in a library reach your program without rebuilding it, which matters a great deal for anything network-facing. Static linking makes sense for single-file distribution, for containers where a tiny image matters, and for rescue tools that must work on a broken system.
Why is statically linking glibc problematic?
glibc’s name service switch loads modules dynamically at runtime, so functions such as getaddrinfo and getpwnam still need the matching shared libraries even in a statically linked binary. The result is a binary that appears static but fails on hostname or user lookups on a different system.
What does LD_LIBRARY_PATH do and should I use it?
It adds directories to the dynamic linker’s search path for a process and its children. It is useful for testing but a poor permanent solution, because it applies to every program launched from that environment and can cause unrelated programs to load the wrong library version.
How do I see which libraries a program uses?
Run ldd on the binary for the list of shared libraries and where each resolves. For a program already running, check /proc/PID/maps to see what is actually mapped, which is more reliable because it reflects what was loaded rather than what was requested.