dynamic-linking

Installation
SKILL.md

动态链接(Dynamic Linking)

用途

引导完成 Linux 动态链接操作:共享库创建、RPATH/RUNPATH 配置、soname 版本管理、dlopen/dlsym 插件模式、LD_PRELOAD 函数拦截以及符号可见性控制。

触发场景

  • "Cannot open shared object file: No such file or directory"
  • "如何设置 RPATH 让二进制文件找到共享库?"
  • "如何使用 dlopen/dlsym 实现插件系统?"
  • "RPATH 和 RUNPATH 有什么区别?"
  • "如何用 LD_PRELOAD 拦截某个函数?"
  • "如何用 soname 对共享库进行版本控制?"

工作流程

1. 创建共享库

# 编译时加 -fPIC(位置无关代码,position-independent code)
gcc -fPIC -c src/mylib.c -o mylib.o

# 带 soname 链接共享库
gcc -shared -Wl,-soname,libmylib.so.1 \
    mylib.o -o libmylib.so.1.2.3

# 创建符号链接(标准约定)
ln -s libmylib.so.1.2.3 libmylib.so.1   # soname 链接(由 ldconfig 维护)
ln -s libmylib.so.1     libmylib.so      # 链接名(编译时使用)

# 系统级注册(通过 ldconfig)
sudo cp libmylib.so.1.2.3 /usr/local/lib/
sudo ldconfig

2. Soname 版本控制约定

libfoo.so.MAJOR.MINOR.PATCH
         └── soname = libfoo.so.MAJOR
版本变更 时机
PATCH 修复 Bug,ABI 不变
MINOR 新增符号,向后兼容
MAJOR ABI 破坏性变更,现有二进制将无法使用

检查 soname:

readelf -d libmylib.so.1.2.3 | grep SONAME
objdump -p libmylib.so.1.2.3 | grep SONAME

3. RPATH 与 RUNPATH

两者都将库搜索路径嵌入二进制文件。

RPATH   → 在 LD_LIBRARY_PATH 之前搜索
RUNPATH → 在 LD_LIBRARY_PATH 之后搜索(可在运行时控制)

建议:优先使用 RUNPATH(-Wl,--enable-new-dtags)
      以获得更好的部署灵活性。
# 嵌入 RPATH(旧默认行为)
gcc main.c -L./lib -lmylib \
    -Wl,-rpath,'$ORIGIN/../lib' -o myapp

# 嵌入 RUNPATH(使用 --enable-new-dtags 后的新默认行为)
gcc main.c -L./lib -lmylib \
    -Wl,-rpath,'$ORIGIN/../lib' \
    -Wl,--enable-new-dtags -o myapp

# 检查
readelf -d myapp | grep -E 'RPATH|RUNPATH'
chrpath -l myapp        # 显示
chrpath -r '/new/path' myapp  # 修改已有路径

$ORIGIN 在运行时解析为二进制文件所在目录,用于可重定位安装。

4. 库搜索顺序

1. DT_RPATH(若无 DT_RUNPATH)
2. LD_LIBRARY_PATH(环境变量,suid 二进制忽略此项)
3. DT_RUNPATH
4. /etc/ld.so.cache(由 ldconfig 根据 /etc/ld.so.conf 填充)
5. /lib, /usr/lib

调试方法:

LD_DEBUG=libs ./myapp      # 追踪库加载决策
ldd myapp                  # 显示已解析的库
ldd -v myapp               # 详细模式,含版本需求

5. dlopen / dlsym 插件模式

#include <dlfcn.h>

typedef int (*plugin_fn_t)(const char *input);

void load_plugin(const char *path) {
    // RTLD_NOW: 立即解析所有符号(尽早失败)
    // RTLD_LAZY: 首次调用时再解析(默认)
    // RTLD_LOCAL: 符号对其他已加载库不可见
    // RTLD_GLOBAL: 符号全局可见
    void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
    if (!handle) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        return;
    }

    // 清除之前的错误
    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);
}

链接时加 -ldl

gcc main.c -ldl -o myapp

6. LD_PRELOAD 函数拦截

LD_PRELOAD 在所有其他库之前加载一个库,其符号会覆盖应用程序中的同名符号。

// myinterpose.c — 拦截 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");  // 在链中查找下一个 malloc

    void *ptr = real_malloc(size);
    fprintf(stderr, "malloc(%zu) = %p\n", size, ptr);
    return ptr;
}
gcc -shared -fPIC -o myinterpose.so myinterpose.c -ldl

# 应用到任意二进制
LD_PRELOAD=./myinterpose.so ./myapp
LD_PRELOAD=/path/to/libfaketime.so ./myapp  # 时间操控

7. 符号可见性控制

限制导出符号可减小 DSO 体积并避免命名冲突:

// 标记为 default:对链接器可见
__attribute__((visibility("default")))
int public_api(void) { return 42; }

// Hidden:内部符号,不导出
__attribute__((visibility("hidden")))
static int internal_helper(void) { return 0; }

或使用链接器版本脚本:

# mylib.map
MYLIB_1.0 {
    global:
        mylib_init;
        mylib_process;
    local:
        *;          # 隐藏其他所有符号
};
gcc -shared -fPIC -Wl,--version-script=mylib.map \
    -o libmylib.so mylib.c

# 检查导出符号
nm -D --defined-only libmylib.so
objdump -T libmylib.so

构建时默认隐藏所有符号,显式标记公开 API:

gcc -shared -fPIC -fvisibility=hidden \
    mylib.c -o libmylib.so

8. 常见错误

错误 原因 解决方法
cannot open shared object file 库不在搜索路径中 设置 RPATH、LD_LIBRARY_PATH,或运行 ldconfig
symbol lookup error: undefined symbol 缺少库或版本不匹配 检查 ldd,添加 -l 标志或修复链接顺序
FATAL: kernel too old 版本需求不匹配 针对旧版 glibc 重新编译
relocation R_X86_64_32 against .rodata 共享库中包含非 PIC 代码 编译时加 -fPIC
version 'GLIBC_2.29' not found 二进制基于新版 glibc 构建 在旧系统上重新构建,或使用 -static

关于 RPATH、soname 和 ld.so 配置的详细说明,参见 references/ld-rpath-soname.md

相关技能

  • 使用 skills/binaries/elf-inspection 检查共享库的节和符号
  • 使用 skills/binaries/linkers-lto 了解链接器标志和符号解析
  • 使用 skills/binaries/binutils 对共享库执行 nm、objdump、strip
  • 使用 skills/compilers/gcc 了解 -fPIC-shared 等编译器标志
Related skills

More from killvxk/low-level-dev-skills-zh

Installs
1
First Seen
Mar 21, 2026