dynamic-linking — Claude Code skill for Linux shared libraries
A skill for Linux dynamic linking: it walks through .so load failures, RPATH vs RUNPATH, soname versioning and dlopen plugin patterns.
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a claude-codeWhat dynamic-linking does
This skill guides an agent through Linux dynamic linking: building shared libraries, configuring RPATH and RUNPATH, soname versioning, dlopen/dlsym plugin patterns, LD_PRELOAD interposition and symbol visibility control. It triggers on its own when a query mentions cannot open shared object file, LD_LIBRARY_PATH, soname or library search order.
Inside are eight sections of commands and code. Building a library with gcc -fPIC and -shared plus a soname, a version-bump table (PATCH, MINOR, MAJOR), the RPATH vs RUNPATH difference with a recommendation to prefer RUNPATH for deployment flexibility, and the full search order from DT_RPATH down to /lib and /usr/lib. Debugging covers LD_DEBUG=libs and ldd -v. There is a C plugin template using RTLD_NOW and RTLD_LOCAL, a malloc interposition example built on dlsym(RTLD_NEXT, ...), and a version script for hiding internal symbols.
You need GCC and the readelf, objdump, chrpath and nm utilities. Plugins link with -ldl. A binary built against a newer glibc will fail on an older system with version 'GLIBC_2.29' not found, so check where your release actually gets built.
What it helps with
- find out why a binary cannot locate its .so at runtime
- pick between RPATH and RUNPATH for a relocatable install
- build a plugin system on dlopen and dlsym
- intercept a function call in someone else's binary with LD_PRELOAD
- hide internal library symbols to avoid clashes
Example prompts for the agent
- why does my binary fail with cannot open shared object file
- set RPATH with $ORIGIN for a relocatable build
- show a dlopen and dlsym plugin example with error handling
How to install dynamic-linking
1Skills CLI (Vercel)
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a claude-code2Manually: put the files in place
~/.claude/skills/dynamic-linking/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/dynamic-linking/
1Skills CLI (Vercel)
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a codex2Manually: put the files in place
.agents/skills/dynamic-linking/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a cursor2Manually: put the files in place
.cursor/skills/dynamic-linking/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install mohitmishra786/low-level-dev-skills dynamic-linking2Manually: put the files in place
.github/skills/dynamic-linking/SKILL.md1Skills CLI (Vercel)
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a gemini-cli2Platform CLI
gemini skills install https://github.com/mohitmishra786/low-level-dev-skills --path skills/binaries/dynamic-linkingManually: put the files in place
.devin/skills/dynamic-linking/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about dynamic-linking
How do I install dynamic-linking in Claude?
Run npx skills add mohitmishra786/low-level-dev-skills --skill dynamic-linking -a claude-code. The skill lands in ~/.claude/skills/dynamic-linking/SKILL.md.
What does dynamic-linking do?
It helps an agent work through Linux dynamic linking: building .so files, soname versioning, RPATH and RUNPATH, dlopen plugins, LD_PRELOAD interposition and symbol visibility.
What is the difference between RPATH and RUNPATH?
RPATH is searched before LD_LIBRARY_PATH, RUNPATH after it, so RUNPATH stays overridable at runtime. The skill recommends RUNPATH via -Wl,--enable-new-dtags.
Source text
SKILL.md · MIT
---
name: dynamic-linking
description: Dynamic linking skill for Linux/ELF shared libraries. Use when debugging library loading failures, configuring RPATH vs RUNPATH, understanding soname versioning, using dlopen/dlsym for plugin systems, LD_PRELOAD interposition, or controlling symbol visibility. Activates on queries about shared libraries, dlopen, LD_LIBRARY_PATH, RPATH, soname, LD_PRELOAD, symbol visibility, or "cannot open shared object file" errors.
---
# 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
```bash
# 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
```
### 2. Soname versioning convention
```text
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:
```bash
readelf -d libmylib.so.1.2.3 | grep SONAME
objdump -p libmylib.so.1.2.3 | grep SONAME
```
### 3. RPATH vs RUNPATH
```text
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.
```
```bash
# 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
```text
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/lib
```
Debug with:
```bash
LD_DEBUG=libs ./myapp # trace library loading decisions
ldd myapp # show resolved libraries
ldd -v myapp # verbose with version requirements
```
### 5. dlopen / dlsym plugin pattern
```c
#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`:
```bash
gcc main.c -ldl -o myapp
```
### 6. LD_PRELOAD interposition
`LD_PRELOAD` loads a library before all others — its symbols override the application's.
```c
// 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;
}
```
```bash
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
```
### 7. Symbol visibility control
Limit exported symbols to reduce binary size and avoid clashes:
```c
// 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:
```text
# mylib.map
MYLIB_1.0 {
global:
mylib_init;
mylib_process;
local:
*; # hide everything else
};
```
```bash
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:
```bash
gcc -shared -fPIC -fvisibility=hidden \
mylib.c -o libmylib.so
```
### 8. 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](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
© mohitmishra786 · MIT · mohitmishra786/low-level-dev-skills