guides / static-vs-dynamic-linking conceptsupdated 2026-10-25 readelf -h ./app

Static vs dynamic linking: how to tell, and why a static build can still fail

2026-10-25 By luckyray Debian 12 · Ubuntu 24.04 · Alpine 3.20

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.

$ 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

DynamicStatic
Kernel startsld.so via PT_INTERPthe program entry point directly
Library resolutionld.so search path at every startalready in the file
Startup costpage faults for each .soone mapping
Patch a libraryrestart the servicerebuild the binary
File sizetens of KB1–10 MB
Fails because ofmissing .so, GLIBC versions, RPATHNSS, 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 static libc.a includes a partial loader for this, but the code it loads still comes from the host.
  • Locale and charset data live in /usr/lib/locale and 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.

Related

guides
  1. musl vs glibc: why an Alpine binary will not run on Ubuntu Both are C libraries; neither is compatible with the other's binaries. Here is how to tell which one a binary was built against, and what the misleading error message really means.
  2. SONAME and library versioning: why libssl.so.3 is not libssl.so.1.1 What a SONAME actually is, how the loader uses it, and why the symlink that 'fixes' a missing library is the reason the program crashes later.
  3. cannot execute binary file: Exec format error — causes and fixes Why Linux refuses to run a binary with Exec format error, how to confirm the cause in three commands, and the five fixes that actually apply.