Dynamic Linker Debug
3 Code Demos
BeginnerβIntermediate
TLPI Ch 42.6
When a shared library fails to load, or a program crashes because a symbol cannot be found, you need to know: Where exactly is the dynamic linker searching? Which library is it picking? Which version is it binding to?
LD_DEBUG is a built-in diagnostic tool in the Linux dynamic linker (ld.so). By setting it to specific keywords, you get detailed trace output about exactly what the linker is doing β library search paths, symbol lookups, relocation processing, version binding, and more.
It requires zero changes to your source code or binary. You just prefix your command with the environment variable and read the output.
| LD_DEBUG=libs ./prog | β | ld.so (dynamic linker) Traces library searches |
β | Debug output to stderr or LD_DEBUG_OUTPUT file |
Run LD_DEBUG=help date to see all options (the program itself won’t execute):
$ LD_DEBUG=help date
Valid options for the LD_DEBUG environment variable are:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
all all previous options combined
statistics display relocation statistics
unused determine unused DSOs
help display this help message and exit
To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.
| Keyword | What it shows | When to use |
|---|---|---|
| libs | Library search paths and which file was selected | Library not found errors, wrong version loaded |
| symbols | Symbol lookup process for each needed symbol | “undefined symbol” errors at runtime |
| bindings | Which library each symbol binding resolves to | Debugging function interposition / wrong function called |
| versions | Version dependency checks for each library | Symbol version mismatch errors |
| reloc | Relocation entries being processed | Low-level linker debugging |
| files | Progress for each input file (library) being loaded | Seeing load order of all libraries |
| unused | Libraries loaded but never actually used | Removing unnecessary link dependencies |
| statistics | Relocation statistics (counts, timing) | Performance analysis of library loading |
| all | All of the above combined | Full diagnostic dump |
This is the most commonly used option. It shows every directory the dynamic linker checks when looking for a library, and which file it eventually loads.
# See where libm.so is being searched and found
LD_DEBUG=libs ls 2>&1 | head -30
# Sample output:
# 7543: find library=libselinux.so.1 [0]; searching
# 7543: search cache=/etc/ld.so.cache
# 7543: /lib/x86_64-linux-gnu/libselinux.so.1
# 7543: trying file=/lib/x86_64-linux-gnu/libselinux.so.1
# 7543:
# 7543: find library=libc.so.6 [0]; searching
# 7543: search cache=/etc/ld.so.cache
# 7543: /lib/x86_64-linux-gnu/libc.so.6
# 7543: trying file=/lib/x86_64-linux-gnu/libc.so.6
# More targeted: check library loading for your own program
LD_DEBUG=libs LD_LIBRARY_PATH=. ./prog 2>&1
# This tells you:
# 1. Which directories were searched (RPATH, LD_LIBRARY_PATH, /etc/ld.so.cache, default dirs)
# 2. Which .so file was actually opened
# 3. Which libraries were NOT found (look for "not found" in output)
7543:) is the process ID (PID). This helps when multiple processes are running and their debug output is interleaved.When you have multiple libraries that define the same function (e.g., when using LD_PRELOAD), bindings tells you exactly which library each call resolved to.
# Using bindings to verify LD_PRELOAD is working correctly
LD_DEBUG=bindings LD_PRELOAD=libalt.so LD_LIBRARY_PATH=. ./prog 2>&1 | grep "x1\|x2"
# Sample output:
# 12345: binding file ./prog [0] to libalt.so [0]: normal symbol `x1'
# 12345: binding file ./prog [0] to libdemo.so [0]: normal symbol `x2'
# Checking which malloc() a program uses (useful after LD_PRELOAD)
LD_DEBUG=bindings LD_PRELOAD=./libmtrace.so ./prog 2>&1 | grep malloc
# Output shows malloc bound to your libmtrace.so (preloaded) not libc
# 99: binding file ./prog to ./libmtrace.so: normal symbol `malloc'
Debug output goes to stderr by default. For long programs, this floods your terminal. Use LD_DEBUG_OUTPUT to redirect it to a file. You can also combine multiple keywords.
# Save all debug output to a file (pid is appended automatically)
LD_DEBUG=libs LD_DEBUG_OUTPUT=/tmp/linker_debug ./prog
# The file will be named /tmp/linker_debug.PID (e.g., /tmp/linker_debug.12345)
ls /tmp/linker_debug.*
# Open the file
less /tmp/linker_debug.12345
# Combine multiple keywords with comma separation
LD_DEBUG=libs,symbols,bindings ./prog 2>&1 | head -50
# Real debugging scenario: "undefined symbol" at runtime
# Step 1: check which libraries load
LD_DEBUG=libs ./prog 2>&1 | grep "find library"
# Step 2: check if the symbol exists in the expected library
nm -D /path/to/libfoo.so | grep my_missing_function
# Step 3: check what version your program needs
readelf -d ./prog | grep NEEDED
ldd ./prog
# Step 4: if library is found but symbol isn't, check symbol lookup
LD_DEBUG=symbols ./prog 2>&1 | grep my_missing_function
# Check for unused libraries (helps reduce binary size / load time)
LD_DEBUG=unused ./prog 2>&1
# Output example:
# 12345: unused DSO: /lib/x86_64-linux-gnu/libm.so.6
# β This means -lm is in your link command but no math functions were called
# β You can remove -lm from your Makefile
LD_DEBUG output always goes to stderr (file descriptor 2), not stdout. To capture it alongside normal output, use 2>&1 to merge stderr into stdout, or redirect with 2>debug.log.Here is a realistic step-by-step workflow for diagnosing a library loading failure:
/* Step 0: The problem β program fails to start */
$ ./prog
./prog: error while loading shared libraries: libfoo.so.1:
cannot open shared object file: No such file or directory
/* Step 1: Check what libraries the program needs */
$ ldd ./prog
linux-vdso.so.1 (0x00007ffc...)
libfoo.so.1 => not found β problem here
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/* Step 2: Find where libfoo.so.1 actually is */
$ find /usr /lib /opt -name "libfoo.so*" 2>/dev/null
/opt/myapp/lib/libfoo.so.1
/* Step 3: See what paths the linker searches */
$ LD_DEBUG=libs ./prog 2>&1 | grep "find library=libfoo"
find library=libfoo.so.1 [0]; searching
search cache=/etc/ld.so.cache
search path=/usr/lib (from RPATH)
trying file=/usr/lib/libfoo.so.1 (not found)
...
/* Step 4a: Quick fix β add path via LD_LIBRARY_PATH */
$ LD_LIBRARY_PATH=/opt/myapp/lib ./prog
/* Step 4b: Permanent fix β add to ldconfig */
$ echo "/opt/myapp/lib" > /etc/ld.so.conf.d/myapp.conf
$ ldconfig # rebuilds /etc/ld.so.cache
/* Verify fix */
$ ldd ./prog
libfoo.so.1 => /opt/myapp/lib/libfoo.so.1 β now found
LD_DEBUG is an environment variable that enables diagnostic tracing in the Linux dynamic linker. By setting it to keywords like libs, symbols, or bindings, you get detailed output about library search paths, symbol lookups, and function bindings. It requires no changes to the binary or source code.LD_DEBUG=libs. This shows every directory the dynamic linker searches when looking for a library, and whether the file was found or not. Combined with ldd, this quickly identifies missing libraries and where to place them.LD_DEBUG_OUTPUT=/path/to/file. The dynamic linker will write its debug output to that file (with the process PID appended as a suffix, e.g., debug.12345) instead of printing to stderr.LD_PRELOAD is working (your override is being called), or for diagnosing cases where the wrong version of a function is being used.-lfoo flag from your Makefile if libfoo.so appears as unused.symbols shows the process of looking up each required symbol β which libraries are searched and in what order. bindings shows the final result of each binding β “symbol X in program Y was bound to library Z”. For most debugging, bindings is more concise and actionable.This chapter covered four advanced features of Linux shared libraries:
| Section | Topic | Key Tool / Feature |
|---|---|---|
| 42.3 | Symbol Versioning | .symver, version script, @/@@ binding |
| 42.4 | Init & Fini Functions | __attribute__((constructor/destructor)) |
| 42.5 | Preloading Libraries | LD_PRELOAD, /etc/ld.so.preload, RTLD_NEXT |
| 42.6 | Dynamic Linker Monitoring | LD_DEBUG=libs/symbols/bindings/unused |
