dynamic-linking
动态链接(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等编译器标志
More from killvxk/low-level-dev-skills-zh
binutils
GNU binutils 二进制操作与分析技能。适用场景:使用 ar 管理静态库、使用 strip 或 objcopy 处理二进制文件、使用 addr2line 将地址转换为源码位置、使用 strings 提取文本、或使用 c++filt 对 C++ 名称进行反混淆。触发条件:涉及 ar、strip、objcopy、addr2line、strings、c++filt、ranlib 或二进制后处理任务的查询。
1ebpf
Linux 可观测性和网络的 eBPF 技能。适用场景:使用 libbpf 或 bpftrace 编写 eBPF 程序、挂载 kprobe/tracepoint/XDP 钩子、调试验证器错误、使用 eBPF map,或实现跨内核版本的 CO-RE 可移植性。触发条件:查询 eBPF、bpftool、bpftrace、XDP 程序、libbpf、验证器错误、eBPF map 或使用 BPF 进行内核追踪相关问题。
1clang
C/C++ 项目的 Clang/LLVM 编译器技能。适用场景:使用 clang 或 clang++ 进行诊断、sanitizer 插桩、优化备注、通过 clang-tidy 进行静态分析、通过 lld 实现 LTO,或从 GCC 迁移到 Clang。触发条件:涉及 clang 标志、clang-tidy、clang-format、更好的错误信息、Apple/FreeBSD 工具链或 LLVM 特定优化的查询。涵盖标志选择、诊断调优及与 LLVM 工具的集成。
1gcc
C/C++ 项目的 GCC 编译器技能。适用场景:选择优化级别、警告标志、调试构建、LTO、sanitizer 插桩,或诊断 GCC 编译错误。涵盖调试与发布构建的标志选择、ABI 问题、预处理器宏、配置引导优化(PGO)及与构建系统的集成。触发条件:涉及 gcc 标志、编译错误、性能调优、警告抑制或跨标准编译的查询。
1cmake
C/C++ 项目的 CMake 构建系统技能。适用场景:编写或重构 CMakeLists.txt、配置源外构建、选择生成器(Ninja、Make、VS)、使用 target_link_libraries 管理目标和依赖、通过 find_package 或 FetchContent 集成外部包、启用 Sanitizer、为交叉编译配置工具链文件,或导出 CMake 包。触发条件:涉及 CMakeLists.txt、cmake 配置错误、目标属性、安装规则、CPack 或 CMake Presets 的查询。
1cpp-modules
现代 C++ 项目的 C++20 模块技能。适用场景:使用命名模块、模块分区、头文件单元、CMake MODULE_SOURCES、Clang -fmodules-ts、BMI 缓存问题,或从头文件迁移到模块。触发条件:涉及 C++20 模块、import 语句、模块接口单元、头文件单元或 BMI 文件的查询。
1