ldd Command in Linux: Complete Guide to Shared Library Dependencies and Parameters

ldd command in Linux and it perimeters

There’s a specific moment in Linux troubleshooting that every sysadmin knows: a binary that runs perfectly on one machine fails with “error while loading shared libraries” on another. That’s the exact problem ldd was built to solve. It’s a small tool, but understanding what it actually does — and its real security caveats — has saved me from some genuinely confusing dependency issues over the years.

What is the ldd Command?

ldd (“list dynamic dependencies”) prints the shared library dependencies of a dynamically linked executable or library, showing exactly which .so files it needs at runtime and where the dynamic linker resolves each one to on disk. It’s essential for debugging missing-library errors, verifying deployment environments, and understanding how a compiled program is actually wired together.

Basic Syntax

ldd [OPTION] FILE...

A Real Example

ldd /bin/ls

Actual output from a test system:

        linux-vdso.so.1 (0x00007f0a2eec3000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f0a2ee5c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0a2ec00000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f0a2eb66000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0a2eec5000)

Reading this line by line:

  • linux-vdso.so.1 — a special virtual shared object provided directly by the kernel (not a real file on disk), used to speed up certain system calls
  • libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 — the library name the binary requested, followed by where the dynamic linker actually resolved it to on disk
  • libc.so.6 — the C standard library, a dependency of nearly every dynamically linked binary on Linux
  • libpcre2-8.so.0 — Perl-Compatible Regular Expressions library, used by ls for pattern matching in some builds
  • /lib64/ld-linux-x86-64.so.2 — the dynamic linker/loader itself, responsible for resolving and loading all the other libraries at program startup
  • The hexadecimal numbers in parentheses are the memory addresses where each library was loaded during this particular invocation

Common Parameters

OptionDescription
-vVerbose output, including version information for symbols
-uReport unused direct dependencies (libraries linked but never actually used)
-dPerform relocations and report any missing functions (data relocations)
-rPerform relocations for both data and functions, reporting any missing symbols
--versionShow ldd‘s own version

Practical Examples

Check dependencies of a custom-compiled binary

ldd ./myapp

The very first thing I run after a fresh build on a new machine — if any line ends in => not found, I know exactly which library is missing before the program even attempts to run.

Find unused linked libraries

ldd -u /usr/bin/somebinary

This flags libraries that were linked at compile time but aren’t actually referenced by any code path — useful when trying to slim down build dependencies or clean up an overly bloated linking configuration.

Verify a library itself, not just an executable

ldd /usr/lib/x86_64-linux-gnu/libssl.so.3

ldd works equally well on shared libraries themselves, showing what they in turn depend on.

Check version symbol resolution

ldd -v /bin/bash

This adds a “Version information” section showing exactly which versioned symbol (e.g., GLIBC_2.34) each dependency satisfies — genuinely useful when diagnosing a “version `GLIBC_2.34′ not found” error after moving a binary to an older system.

Interpreting a Missing Dependency

When a library can’t be found, ldd shows it clearly:

        libcustom.so.1 => not found

This is the exact signature of the classic “error while loading shared libraries: libcustom.so.1: cannot open shared object file” runtime error — ldd lets you diagnose it before actually running the broken binary and getting a less informative crash.

How ldd Works Internally

This is genuinely important to understand, not just trivia: ldd, in its default mode, works by actually executing the target binary with a special environment variable set (LD_TRACE_LOADED_OBJECTS=1) that tells the dynamic linker (ld.so/ld-linux.so) to print out the resolved library paths instead of actually running the program’s normal code path.

This has a serious, well-documented security implication: running ldd on an untrusted or malicious binary can execute arbitrary code, because the dynamic linker itself processes constructors and certain initialization routines as part of resolving dependencies. This is exactly why the ldd manual page explicitly warns against running it on files you don’t trust.

For statically linked binaries — executables compiled with all their dependencies baked directly into the binary rather than resolved at runtime — ldd will report either “not a dynamic executable” or, depending on your distro’s specific ldd implementation, may attempt (and fail safely) to trace it anyway.

A Safer Alternative for Untrusted Binaries

Instead of running ldd directly on a binary you don’t fully trust, use objdump or readelf to statically inspect the dependency list without ever executing the file:

readelf -d /path/to/suspicious-binary | grep NEEDED

or

objdump -p /path/to/suspicious-binary | grep NEEDED

Both list the same DT_NEEDED entries from the binary’s ELF header without invoking the dynamic linker at all — the correct approach for anything you haven’t already verified as trustworthy.

Real-World Use Cases

Diagnosing “error while loading shared libraries” after deployment:

ldd /opt/myapp/bin/server

Immediately reveals which specific .so file is missing on the target machine, letting me install exactly the right package rather than guessing.

Verifying a Docker image has all required runtime libraries:

docker run --rm myimage ldd /usr/local/bin/myapp

I run this as part of image-build verification — if a multi-stage Docker build accidentally leaves out a runtime library that was only present in the build stage, this catches it immediately.

Confirming which OpenSSL version a binary actually links against (relevant to security patching):

ldd /usr/sbin/nginx | grep ssl

Investigating a version mismatch after an OS upgrade:

ldd -v /usr/bin/python3 | grep GLIBC

Finding Which Package Provides a Missing Library

Once ldd tells you a library is missing, the next step is finding which package provides it.

On Debian/Ubuntu:

apt-cache search libcustom.so.1
# or, if apt-file is installed:
apt-file search libcustom.so.1

On RHEL/CentOS/Fedora:

dnf provides '*/libcustom.so.1'

Troubleshooting Common Issues

“not a dynamic executable” — The binary is statically linked and has no shared library dependencies to list — this is expected, not an error.

Library resolves to an unexpected path — This often points to LD_LIBRARY_PATH being set to something unexpected, or multiple versions of a library installed in different directories with the wrong one taking precedence. Check with:

echo $LD_LIBRARY_PATH
ldconfig -p | grep libname

ldd reports a library as found, but the program still fails at runtime — This can happen when the correct library file exists but is missing a required symbol (e.g., after a partial or mismatched upgrade). The -r or -d flags, which perform actual relocation checks, will surface these more subtle problems that a plain ldd run misses.

Different results between architectures — On systems supporting multiple architectures (e.g., both 32-bit and 64-bit libraries via multiarch), make sure you’re running ldd against a binary of the matching architecture, or the results will be misleading or the tool may refuse to trace it entirely.

Security Implications

This bears repeating clearly: never run plain ldd on a binary you don’t trust. Because default ldd behavior actually executes the target file (via the LD_TRACE_LOADED_OBJECTS mechanism), a malicious binary can execute arbitrary code the moment you run ldd against it — this is documented, known behavior, not an obscure edge case. For any binary of unknown origin, use readelf -d or objdump -p instead, which parse the ELF headers statically without ever executing the file.

ldd vs. Related Commands

CommandDifference
readelf -dStatically inspects ELF dependency headers without executing the binary — safer for untrusted files
objdump -pSimilar static inspection, more general-purpose ELF/object file analysis
ldconfig -pLists the cache of all shared libraries known to the dynamic linker system-wide, rather than dependencies of one specific binary
nmLists symbols within a binary or library, useful for finding exactly which function is missing rather than which library
straceCan show actual runtime library loading behavior (via openat() calls) as a program executes, useful when ldd‘s static resolution doesn’t match real runtime behavior

Compatibility Across Distributions

ldd ships as part of glibc on virtually every mainstream Linux distribution — Debian, Ubuntu, RHEL, CentOS, Fedora, Arch, openSUSE — and its output format is consistent across all of them, since it’s fundamentally a thin wrapper exposing the dynamic linker’s own dependency-tracing capability. Systems using an alternative C library, like musl libc (common on Alpine Linux), provide their own compatible ldd implementation with the same basic interface, though verbose/version-related flags may behave slightly differently.

Static vs. Dynamic Linking — Why This All Matters

To really understand what ldd is telling you, it helps to understand the two fundamental ways a compiled program can be built on Linux:

  • Static linking bakes every library dependency directly into the final executable at compile time, producing a single, larger, self-contained binary that doesn’t need external .so files at runtime.
  • Dynamic linking keeps library code in separate .so files on disk, and the executable only stores references to what it needs. The dynamic linker (ld.so) resolves and loads these at the moment the program starts.

Dynamic linking is the overwhelming default for most software on Linux, because it saves disk space (multiple programs share one copy of libc, for example) and lets you update a shared library (say, patching a security vulnerability in OpenSSL) without needing to recompile every single program that depends on it. That convenience is exactly why ldd exists — you constantly need to verify these dynamic dependencies are actually satisfiable on whatever machine you’re deploying to.

You can check whether a binary is statically or dynamically linked with file:

file /bin/ls

Typical output:

/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...

The phrase “dynamically linked” confirms ldd will actually have something meaningful to report.

Understanding rpath, runpath, and LD_LIBRARY_PATH Resolution Order

When ldd resolves a library name to an actual file path, it’s replicating the exact search order the dynamic linker itself follows at runtime:

  1. Any DT_RPATH entries embedded directly in the binary (older mechanism, now largely superseded)
  2. LD_LIBRARY_PATH environment variable, if set
  3. Any DT_RUNPATH entries embedded in the binary (the modern replacement for rpath)
  4. The system’s dynamic linker cache, built from /etc/ld.so.conf and /etc/ld.so.conf.d/*.conf, and viewable directly with ldconfig -p
  5. Default system directories like /lib and /usr/lib

Understanding this order matters enormously when you have multiple versions of the same library installed (a common scenario with things like OpenSSL or Python) and need to figure out exactly which one a given binary will actually pick up.

ldconfig -p | grep libssl

This shows every version of libssl the dynamic linker’s cache currently knows about, which — cross-referenced with ldd‘s resolved path for a specific binary — tells you exactly which version that binary is actually going to use.

A Practical Debugging Session

Here’s a realistic troubleshooting flow I’ve gone through more times than I can count, moving a compiled binary to a fresh server:

$ ./myapp
./myapp: error while loading shared libraries: libfoo.so.2: cannot open shared object file: No such file or directory

$ ldd ./myapp
        linux-vdso.so.1 (0x00007fff00000000)
        libfoo.so.2 => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
        /lib64/ld-linux-x86-64.so.2 (0x00007f...)

$ find / -name 'libfoo.so*' 2>/dev/null
/opt/legacy-libs/libfoo.so.2

$ export LD_LIBRARY_PATH=/opt/legacy-libs:$LD_LIBRARY_PATH
$ ldd ./myapp
        libfoo.so.2 => /opt/legacy-libs/libfoo.so.2 (0x00007f...)

$ ./myapp
# runs successfully

For a permanent fix rather than a session-scoped LD_LIBRARY_PATH export, I’d typically add the directory to /etc/ld.so.conf.d/ and run sudo ldconfig to rebuild the cache instead.

Summary

ldd is one of those tools that looks trivial on the surface — just list some file paths — but understanding how it actually works internally (that it executes the target file via the dynamic linker) changes how carefully I use it. For everyday dependency debugging on binaries I trust, it’s fast and exactly what I need. For anything unverified, I reach for readelf -d instead, and that one habit has kept me out of trouble more than once.

References

  • Linux man-pages project, ldd(1): https://man7.org/linux/man-pages/man1/ldd.1.html
  • Linux man-pages project, ld.so(8): https://man7.org/linux/man-pages/man8/ld.so.8.html
  • GNU C Library (glibc) documentation: https://www.gnu.org/software/libc/manual/
  • Linux man-pages project, readelf(1): https://man7.org/linux/man-pages/man1/readelf.1.html
Total
0
Shares

Leave a Reply

Previous Post
fg command in Linux and it perimeters

fg Command in Linux: Complete Guide to Foreground Process Management and Parameters

Next Post
nice command in Linux and it perimeters

nice Command in Linux: Complete Guide to Process Priority Adjustment and Parameters

Related Posts