Static vs dynamic linking: how to tell, and why a static build can still fail
A binary is dynamically linked if it contains an interpreter path and a list of libraries to load. It is statically linked if it contains neither. That is the whole structural definition, and it is recorded in two places.
Identifying the link mode
$ readelf -l ./app | grep -E 'INTERP'
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
$ readelf -d ./app | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Both present: dynamic. Neither present: static. If readelf -l prints nothing for INTERP but
readelf -d shows NEEDED entries, you have a static-PIE or a statically linked loader setup —
rare, but it happens with -static-pie.
file gives the short version:
$ file ./app
./app: ELF 64-bit LSB executable, x86-64, ..., statically linked, stripped
$ file ./other
./other: ELF 64-bit LSB pie executable, x86-64, ..., dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, stripped
What actually changes at run time
| Dynamic | Static | |
|---|---|---|
| Kernel starts | ld.so via PT_INTERP | the program entry point directly |
| Library resolution | ld.so search path at every start | already in the file |
| Startup cost | page faults for each .so | one mapping |
| Patch a library | restart the service | rebuild the binary |
| File size | tens of KB | 1–10 MB |
| Fails because of | missing .so, GLIBC versions, RPATH | NSS, dlopen, locale data, /etc config |
The last row of the dynamic column is why static builds look attractive: they remove an entire class of shared-library failures and all GLIBC symbol-version problems at once. That part is true.
Why a static glibc binary is not portable
The linker will happily produce a static binary and then warn you:
$ gcc -static -O2 -o app app.c
/usr/bin/ld: /usr/lib/x86_64-linux-gnu/libc.a(getpwnam.o): in function `getpwnam':
(.text+0x120): warning: Using 'getpwnam' in statically linked applications requires at runtime
the shared libraries from the glibc version used for linking
The warning is not boilerplate. getpwnam(), getgrnam(), gethostbyname() and friends are thin
wrappers that, in glibc, dispatch into NSS modules — plugins loaded from /lib/.../libnss_*.so.2
at run time. A static binary carries the wrapper but not the modules, and it opens them by name from
the filesystem. Copy that binary to a system with different NSS modules and user lookups start
failing in ways that look like config errors.
Two related consequences:
dlopen()from a static binary requires the dynamic loader to exist anyway. glibc’s staticlibc.aincludes a partial loader for this, but the code it loads still comes from the host.- Locale and charset data live in
/usr/lib/localeand are read at run time. Static linking never bundles them.
The portable alternative: musl static
musl-gcc -static -O2 -o app app.c
# or, on Alpine:
apk add build-base && cc -static -o app app.c
musl’s static libc has no NSS indirection: user and group lookups read /etc/passwd and /etc/group
directly. A musl static binary is genuinely self-contained for most programs, which is why it is the
default for container-friendly CLIs and for tools distributed as a single file.
The trade-off is behavioural, not structural: musl has a different malloc (slower for
allocation-heavy workloads), no iconv for arbitrary charsets, different dlopen support, and it
implements only the DNS resolver configuration it can parse. Software that assumed glibc-specific
behaviour will break in ways that no linker flag fixes.
Checking a binary you were handed
$ file ./vendor-cli
./vendor-cli: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux),
statically linked, BuildID[sha1]=..., stripped
That says static — but not which libc. Check for musl fingerprints:
$ strings ./vendor-cli | grep -E 'musl|GLIBC_[0-9]' | head -5
musl libc (x86_64)
Version 1.2.4
musl libc in the strings output plus no GLIBC_ version strings means a musl static build, which
is the good case. A static glibc build instead shows GLIBC_2.3x strings and will still need host
NSS modules.
Rule of thumb
- Distributing to unknown Linux hosts: musl static, or ship a container image.
- Distributing inside one distribution: dynamic, built against that distribution’s oldest supported release.
- Distributing to a fleet you control with a package manager: dynamic, always. Static binaries bypass security updates, and a statically linked OpenSSL means CVE day is rebuild day.
The ELF Inspector reports statically linked, the interpreter path and the
libc family in the summary tab, so you can answer this without a shell on the target.
Every command in this guide was run against the distribution named in the output. If your output differs, the mechanism section explains why — and the ELF Inspector shows the same fields without needing a shell.