
Dynamic Linking
- 340 installs
- 155 repo stars
- Updated June 27, 2026
- mohitmishra786/low-level-dev-skills
Load shared objects at runtime, resolve symbols across plugins, and ship hot-swappable native extensions for CLIs, servers, and desktop components on Linux and Unix.
About
Covers designing and debugging dynamic linking on Unix: building .so files, runtime loading, symbol resolution, and loader paths. Helps teams add plugin architectures to CLIs, native APIs, and browser-style extension hosts.
- Shared library creation and SONAME
- dlopen, dlsym, and dlclose usage
- Symbol visibility and export maps
- RPATH, RUNPATH, and loader search order
- ABI stability across releases
Dynamic Linking by the numbers
- 340 all-time installs (skills.sh)
- +24 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,226 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mohitmishra786/low-level-dev-skills --skill dynamic-linkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 340 |
|---|---|
| repo stars | ★ 155 |
| Last updated | June 27, 2026 |
| Repository | mohitmishra786/low-level-dev-skills ↗ |
What it does
Load shared objects at runtime, resolve symbols across plugins, and ship hot-swappable native extensions for CLIs, servers, and desktop components on Linux and Unix.
Files
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
1. 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 ldconfig2. 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 SONAME3. 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.
4. Library search order
1. DT_RPATH (if no DT_RUNPATH present)
2. LD_LIBRARY_PATH (env var, ignored for suid binaries)
3. DT_RUNPATH
4. /etc/ld.so.cache (populated by ldconfig from /etc/ld.so.conf)
5. /lib, /usr/libDebug with:
LD_DEBUG=libs ./myapp # trace library loading decisions
ldd myapp # show resolved libraries
ldd -v myapp # verbose with version requirements5. 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 myapp6. 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 manipulation7. 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.soBuild with -fvisibility=hidden by default and explicitly mark public API:
gcc -shared -fPIC -fvisibility=hidden \
mylib.c -o libmylib.so8. 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-inspectionto inspect shared library sections and symbols - Use
skills/binaries/linkers-ltofor linker flags and symbol resolution - Use
skills/binaries/binutilsfornm,objdump,stripon shared libs - Use
skills/compilers/gccfor-fPIC,-sharedand related compiler flags
RPATH, RUNPATH, and Soname Reference
ld.so Search Path Configuration
System-wide (/etc/ld.so.conf)
# /etc/ld.so.conf.d/mylib.conf
/usr/local/lib/myapp
/opt/myapp/lib# After editing conf files:
sudo ldconfig
# Verify
ldconfig -p | grep libmylibPer-user (LD_LIBRARY_PATH)
export LD_LIBRARY_PATH=/home/user/mylibs:$LD_LIBRARY_PATH
./myapp
# Avoid in production — security risk for suid binaries
# Use RUNPATH instead for deployable binariesRPATH / RUNPATH Deep Dive
$ORIGIN Patterns
| Pattern | Resolves to |
|---|---|
$ORIGIN | Directory containing the binary |
$ORIGIN/../lib | lib/ sibling directory |
$ORIGIN/../../lib | Two levels up, then lib/ |
$LIB | Architecture lib dir (e.g., lib/x86_64-linux-gnu) |
$PLATFORM | Platform string (e.g., x86_64) |
# Package layout using $ORIGIN
myapp/
├── bin/
│ └── myapp # RUNPATH = $ORIGIN/../lib
└── lib/
├── libfoo.so.1
└── libbar.so.2Modifying Existing RPATH
# View
patchelf --print-rpath ./myapp
chrpath -l ./myapp
# Change
patchelf --set-rpath '$ORIGIN/../lib' ./myapp
chrpath -r '$ORIGIN/../lib' ./myapp
# Remove
patchelf --remove-rpath ./myapp
chrpath -d ./myappCMake RPATH Configuration
# Set install RPATH
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
# Include build tree RPATH in build (useful for testing)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
# Add default install path to RPATH
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# Use RUNPATH (new dtags) instead of RPATH
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--enable-new-dtags")Soname Versioning Lifecycle
Creating a versioned library
# Compile
gcc -fPIC -c libfoo.c -o libfoo.o
# Link with soname
gcc -shared -Wl,-soname,libfoo.so.1 \
libfoo.o -o libfoo.so.1.0.0
# Symlinks
ln -sf libfoo.so.1.0.0 libfoo.so.1 # soname — what ldconfig maintains
ln -sf libfoo.so.1 libfoo.so # linker name — what -lfoo usesUpgrading (minor ABI-compatible)
# New release
gcc -shared -Wl,-soname,libfoo.so.1 \
libfoo.o -o libfoo.so.1.1.0
# Update only libfoo.so.1 symlink; libfoo.so.1.0.0 stays for rollback
ln -sf libfoo.so.1.1.0 libfoo.so.1
sudo ldconfigBreaking ABI (major bump)
# New major version
gcc -shared -Wl,-soname,libfoo.so.2 \
libfoo.o -o libfoo.so.2.0.0
ln -sf libfoo.so.2.0.0 libfoo.so.2
ln -sf libfoo.so.2 libfoo.so
# Old libfoo.so.1* stays installed for binaries linked against it
sudo ldconfigVersion Scripts (GNU ld)
# libfoo.map — symbol versioning
LIBFOO_1.0 {
global:
foo_init;
foo_process;
foo_cleanup;
local:
*;
};
LIBFOO_1.1 {
global:
foo_process_ex; # New in 1.1
} LIBFOO_1.0; # Inherits 1.0 symbolsgcc -shared -Wl,--version-script=libfoo.map \
-o libfoo.so.1 libfoo.o
# Check versioned symbols
readelf -s --wide libfoo.so.1 | grep LIBFOODebugging Dynamic Linking
# Verbose library resolution
LD_DEBUG=libs ./myapp 2>&1 | head -50
# All linker debug options
LD_DEBUG=help ./myapp
# Common LD_DEBUG values:
# libs — library search
# symbols — symbol lookup
# bindings — symbol binding
# files — input files processed
# all — everything (very verbose)
# Check what a binary needs
ldd -v ./myapp
# Check for missing symbols before running
ldd ./myapp | grep "not found"
# Simulate different library versions
LD_PRELOAD=/path/to/alternate/libfoo.so.1 ./myapp