Dynamic Linking
Purpose
Guide agents through Linux dynamic linking: shared library creation, RPATH/RUNPATH configuration, soname versioning, dlopen /dlsym plugin patterns, LD_PRELOAD interposition, and symbol visibility control.
Triggers
-
"Cannot open shared object file: No such file or directory"
-
"How do I set RPATH so my binary finds its shared library?"
-
"How do I use dlopen/dlsym for a plugin system?"
-
"What's the difference between RPATH and RUNPATH?"
-
"How do I use LD_PRELOAD to intercept a function?"
-
"How do I version my shared library with soname?"
Workflow
- Creating a shared library
Compile with -fPIC (position-independent code)
gcc -fPIC -c src/mylib.c -o mylib.o
Link shared library with soname
gcc -shared -Wl,-soname,libmylib.so.1
mylib.o -o libmylib.so.1.2.3
Create symlinks (standard convention)
ln -s libmylib.so.1.2.3 libmylib.so.1 # soname link (used by ldconfig) ln -s libmylib.so.1 libmylib.so # link link (used at compile time)
Register with ldconfig (system-wide)
sudo cp libmylib.so.1.2.3 /usr/local/lib/ sudo ldconfig
- Soname versioning convention
libfoo.so.MAJOR.MINOR.PATCH │ └── soname = libfoo.so.MAJOR
Version bump When
PATCH Bug fix, ABI unchanged
MINOR New symbols added, backwards compatible
MAJOR ABI break — existing binaries will break
Inspect soname:
readelf -d libmylib.so.1.2.3 | grep SONAME objdump -p libmylib.so.1.2.3 | grep SONAME
- RPATH vs RUNPATH
Both embed a library search path in the binary.
RPATH → searched BEFORE LD_LIBRARY_PATH RUNPATH → searched AFTER LD_LIBRARY_PATH (controllable at runtime)
Recommendation: prefer RUNPATH (-Wl,--enable-new-dtags) for deployment flexibility.
Embed RPATH (old default)
gcc main.c -L./lib -lmylib
-Wl,-rpath,'$ORIGIN/../lib' -o myapp
Embed RUNPATH (new default with --enable-new-dtags)
gcc main.c -L./lib -lmylib
-Wl,-rpath,'$ORIGIN/../lib'
-Wl,--enable-new-dtags -o myapp
Inspect
readelf -d myapp | grep -E 'RPATH|RUNPATH' chrpath -l myapp # show chrpath -r '/new/path' myapp # modify existing
$ORIGIN resolves to the directory of the binary at runtime — use it for relocatable installations.
-
Library search order
-
DT_RPATH (if no DT_RUNPATH present)
-
LD_LIBRARY_PATH (env var, ignored for suid binaries)
-
DT_RUNPATH
-
/etc/ld.so.cache (populated by ldconfig from /etc/ld.so.conf)
-
/lib, /usr/lib
Debug with:
LD_DEBUG=libs ./myapp # trace library loading decisions ldd myapp # show resolved libraries ldd -v myapp # verbose with version requirements
- dlopen / dlsym plugin pattern
#include <dlfcn.h>
typedef int (*plugin_fn_t)(const char *input);
void load_plugin(const char *path) { // RTLD_NOW: resolve all symbols immediately (fail fast) // RTLD_LAZY: resolve on first call (default) // RTLD_LOCAL: symbols not visible to other loaded libs // RTLD_GLOBAL: symbols visible globally void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); if (!handle) { fprintf(stderr, "dlopen: %s\n", dlerror()); return; }
// Clear previous errors
dlerror();
plugin_fn_t fn = (plugin_fn_t)dlsym(handle, "plugin_run");
const char *err = dlerror();
if (err) {
fprintf(stderr, "dlsym: %s\n", err);
dlclose(handle);
return;
}
fn("hello");
dlclose(handle);
}
Link with -ldl :
gcc main.c -ldl -o myapp
- LD_PRELOAD interposition
LD_PRELOAD loads a library before all others — its symbols override the application's.
// myinterpose.c — intercept malloc #define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h>
void *malloc(size_t size) { static void *(*real_malloc)(size_t) = NULL; if (!real_malloc) real_malloc = dlsym(RTLD_NEXT, "malloc"); // find next malloc in chain
void *ptr = real_malloc(size);
fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
return ptr;
}
gcc -shared -fPIC -o myinterpose.so myinterpose.c -ldl
Apply to any binary
LD_PRELOAD=./myinterpose.so ./myapp LD_PRELOAD=/path/to/libfaketime.so ./myapp # time manipulation
- Symbol visibility control
Limit exported symbols to reduce binary size and avoid clashes:
// Mark default: visible to linker attribute((visibility("default"))) int public_api(void) { return 42; }
// Hidden: internal, not exported attribute((visibility("hidden"))) static int internal_helper(void) { return 0; }
Or use a linker version script:
mylib.map
MYLIB_1.0 { global: mylib_init; mylib_process; local: *; # hide everything else };
gcc -shared -fPIC -Wl,--version-script=mylib.map
-o libmylib.so mylib.c
Check exported symbols
nm -D --defined-only libmylib.so objdump -T libmylib.so
Build with -fvisibility=hidden by default and explicitly mark public API:
gcc -shared -fPIC -fvisibility=hidden
mylib.c -o libmylib.so
- Common errors
Error Cause Fix
cannot open shared object file
Library not in search path Set RPATH, LD_LIBRARY_PATH , or run ldconfig
symbol lookup error: undefined symbol
Missing library or wrong version Check ldd , add -l flag or fix link order
FATAL: kernel too old
Version requirement mismatch Rebuild against older glibc
relocation R_X86_64_32 against .rodata
Non-PIC code in shared lib Add -fPIC to compilation
version 'GLIBC_2.29' not found
Binary built on newer glibc Rebuild on older system or use -static
For RPATH, soname, and ld.so configuration details, see references/ld-rpath-soname.md.
Related skills
-
Use skills/binaries/elf-inspection to inspect shared library sections and symbols
-
Use skills/binaries/linkers-lto for linker flags and symbol resolution
-
Use skills/binaries/binutils for nm , objdump , strip on shared libs
-
Use skills/compilers/gcc for -fPIC , -shared and related compiler flags