tools / dependency-doctor DT_NEEDEDRPATH · RUNPATHldd cross-check readelf -d ./app

Dependency Doctor

error while loading shared libraries names one library and stops. This tool shows the whole dependency list the loader will walk, the search paths baked into the binary, and — if you paste your ldd output — which entries actually resolved on the host.

Binary + optional loader output nothing leaves your browser
Drop the ELF binary here or click to choose a file

How the loader actually searches

When you execute a dynamically linked binary the kernel reads PT_INTERP, starts that dynamic loader, and hands it the file. The loader then resolves each DT_NEEDED entry by walking a fixed search order. Getting this order wrong is the single most common reason a library is "installed but not found":

  1. DT_RPATH — only if the binary has no DT_RUNPATH. It sits before LD_LIBRARY_PATH, which is why a stale RPATH can shadow the library you just exported.
  2. LD_LIBRARY_PATH — the environment override, ignored for setuid binaries.
  3. DT_RUNPATH — the modern replacement; deliberately searched after the environment.
  4. /etc/ld.so.cache — the compiled index maintained by ldconfig.
  5. /lib, /usr/lib and their multiarch equivalents.

Every path in that list is a directory search for a filename, not a version match. The loader does not know what version you meant — it knows what filename the binary recorded in DT_NEEDED. That is why installing OpenSSL 3 on a distribution expecting OpenSSL 1.1 produces "missing libssl.so.1.1": the file genuinely does not exist under that name, even though "openssl is installed".

Why ldd is useful but not safe

ldd is a shell script that runs the loader in a special mode; on some systems it executes the binary's dependencies. Never run ldd against a binary you do not trust — the documented alternative is objdump -p file | grep NEEDED, which only reads the file. This tool does the same thing structurally: it parses DT_NEEDED out of the dynamic section and never executes anything.

Combining both gives the useful answer. The binary tells you what it wants; the ldd output tells you what the host resolved. Pasting the loader output here maps one onto the other, so a library that is not found is highlighted next to its position in the NEEDED list, along with the search paths that were in effect.

Static binaries and glibc are not the same thing

A statically linked binary has no PT_INTERP and no DT_NEEDED: there is nothing to resolve and no loader involved. A binary linked against glibc's static libc.a is still not portable to a musl system in the other direction — it simply does not need musl at all, which mostly works, but calls like getpwnam() that depend on NSS modules will fail silently at runtime. If the goal is "one binary that runs everywhere", the answer is usually musl-gcc -static or shipping the loader and libraries alongside, not a glibc static build.