Trace Shared Libraries with ld.so Safely

ld.so is the dynamic linker every program on your system quietly relies on. Invoking it directly shows exactly which libraries a program needs, where each was found, and which loader settings affect the search. This guide is entirely read-only: nothing here edits the cache, configuration files or the program itself. Allow about fifteen minutes. You need a dynamically linked ELF program, a shell and ordinary read access to it.

This guide describes glibc 2.39 on the machine used for these checks. The installed ld.so(8) page is from Linux man-pages 6.7. Paths and diagnostic details can differ on another architecture or distribution.

1. Identify the loader and the ELF program

The dynamic linker is normally started for you through the ELF program's .interp entry. On this x86-64 system the interpreter is /lib64/ld-linux-x86-64.so.2. Check both values before using a direct loader command:

$ getconf GNU_LIBC_VERSION
glibc 2.39
$ readelf -l /bin/echo | grep 'Requesting program interpreter'
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
$ /lib64/ld-linux-x86-64.so.2 --version | head -1
ld.so (Ubuntu GLIBC 2.39-0ubuntu8.9) stable release version 2.39.

Use a different executable if /bin/echo is not present. readelf reads the ELF program header table; it does not run the target. In ELF terms, that table describes the segments needed to load the file, while the dynamic section records information such as required shared objects and search paths.

Checkpoint: you have the interpreter path printed by readelf, and it exists as an executable file. Do not guess a loader path from a 32-bit example on a 64-bit host.

2. List dependencies and their resolved paths

Run the loader directly with --list. This invokes the selected loader for the target and lists how its dependencies would be resolved. It does not give the target its normal opportunity to do useful work, but a harmless executable is fine while you learn the output:

$ /lib64/ld-linux-x86-64.so.2 --list /bin/echo
        linux-vdso.so.1 (0x0000...)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000...)
        /lib64/ld-linux-x86-64.so.2 (0x0000...)

The addresses are examples and will change because of address-space layout randomisation. The useful part is the mapping, such as libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6. If a dependency cannot be found, the loader reports that instead of producing a usable mapping.

For a different target, replace /path/to/program with an executable you trust:

$ /lib64/ld-linux-x86-64.so.2 --list /path/to/program

Do not use this form as a substitute for normal execution when the target has side effects. The direct invocation selects the loader explicitly, and loader options can change the environment the target starts in.

3. Check whether a file is a compatible dynamic program

--verify checks that the supplied file is dynamically linked and that the chosen loader can handle it:

$ /lib64/ld-linux-x86-64.so.2 --verify /bin/echo
$ printf 'loader check status: %s\n' "$?"
loader check status: 0

A zero status is a compatibility check, not proof that every one of the program's libraries is available or that it will run correctly. A statically linked program has no dynamic dependencies to resolve, and a program for another architecture needs its matching interpreter and libraries.

Checkpoint: use --verify first when a script is unsure whether a file is a suitable loader target. If it fails, inspect the file with file and readelf -h rather than repeatedly changing library paths.

4. Read the search order before changing a path

For a dependency name without a slash, the loader considers the program's ELF metadata and runtime settings in a defined order:

  1. An old DT_RPATH, used when there is no DT_RUNPATH.
  2. LD_LIBRARY_PATH, unless secure-execution mode applies.
  3. DT_RUNPATH.
  4. The compiled cache at /etc/ld.so.cache.
  5. The default directories, such as /lib and /usr/lib.

DT_RUNPATH applies to direct dependencies only, whereas DT_RPATH also affects searches for children further down the dependency tree.

Inspect the program's dynamic entries without changing it:

$ readelf -d /bin/echo | grep -E 'NEEDED|RPATH|RUNPATH'
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Your output may include a RUNPATH or no path entries at all. The NEEDED name is not itself a pathname; the loader combines it with the search rules above.

5. Test a temporary library path safely

For diagnosis, --library-path supplies a path for this direct loader invocation instead of the LD_LIBRARY_PATH setting. Useful for testing an unpacked application, or isolating a missing-library report:

$ /lib64/ld-linux-x86-64.so.2 \
    --library-path '/path/to/app/lib:/path/to/app/lib64' \
    --list /path/to/app/bin/program

Use existing directories that hold libraries compatible with the target; the colon separates entries. The loader also expands $ORIGIN, $LIB and $PLATFORM in this option, so quote those tokens or the shell will expand them first:

$ /lib64/ld-linux-x86-64.so.2 \
    --library-path '$ORIGIN/../$LIB' \
    --list /path/to/app/bin/program

A mistyped directory does not install anything, but it can make the diagnostic misleading. Confirm the final mappings with --list.

6. Use loader diagnostics, not system-wide preload files

LD_DEBUG=libs prints library search information, and LD_DEBUG=help lists the available categories. Keep the setting scoped to one command:

$ LD_DEBUG=libs /bin/echo loader-check 2> /tmp/ld-debug.txt
$ sed -n '1,24p' /tmp/ld-debug.txt
      ...: find library=libc.so.6 [0]; searching
      ...:  search cache=/etc/ld.so.cache
      ...:  trying file=/lib/x86_64-linux-gnu/libc.so.6

The exact prefixes and candidate paths vary. The file in /tmp is diagnostic output and can be removed once you have looked at it.

Warning: do not set LD_PRELOAD globally or edit /etc/ld.so.preload for routine troubleshooting. Preloading changes every subsequently executed program, can prevent commands from starting, and requires elevated privileges for the system file. If you have already made a temporary shell export, undo it with unset LD_PRELOAD LD_LIBRARY_PATH LD_DEBUG; a file edit requires restoring its previous contents from a known-good backup and checking affected services.

Secure-execution mode changes this picture. When the kernel supplies a non-zero AT_SECURE, commonly for set-user-ID or set-group-ID programs, the loader ignores or strips sensitive variables including LD_LIBRARY_PATH, LD_PRELOAD and LD_DEBUG. Never treat an environment variable as a security boundary.

Done means