理解链接:加载二进制动态库
文章目录
- 理解链接:加载二进制动态库
- 前情提要
- 基本方式1 - 显式连接 dlopen
- 基本方式 2 - 隐式链接 compile + link + ld
- 衍生方式 3 - 弱链接 weak linking
- 衍生方式 4 - dlmopen 加载到独立命名空间
- 调试所有符号
- 补充知识
- 1. 动态库的创建与编译
- 2. 动态库的搜索路径
- 3. 符号可见性控制
- 4. 动态库的版本控制
- 5. 动态库的初始化与清理函数
- 6. 调试工具
- 7. 安全性注意事项
前情提要
之前做的 rev 题一般动态库都是直接在编译时 link 好了动态库,从反编译代码里看不出来调用的过程
机缘巧合仔细了解一下才发现二进制的调库原来跟 python, js 这种语言的依赖引用是很像的,把库以二进制 dll/so 的形式放在 path 下面,让系统能找到,然后用路径和名称调用符号就行了。
基本方式1 - 显式连接 dlopen
runtime linking / manually linking
通过 dlopen 可以打开任意动态库,然后通过函数指针使用里面的函数。
int main() {
// 加载共享库,RTLD_LAZY 表示延迟解析符号(只有当实际调用时才解析)
void *handle = dlopen("libm.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen error: %s\n", dlerror());
exit(EXIT_FAILURE);
}
// 清除之前的错误
dlerror();
// 从库中查找符号,例如查找数学库中的 cos 函数
double (*cos_func)(double) = (double (*)(double)) dlsym(handle, "cos");
char *error = dlerror();
if (error != NULL) {
fprintf(stderr, "dlsym error: %s\n", error);
dlclose(handle);
exit(EXIT_FAILURE);
}
// 调用动态加载的函数
double result = cos_func(2.0);
printf("cos(2.0) = %f\n", result);
// 使用完毕后关闭共享库
dlclose(handle);
return 0;
}
基本方式 2 - 隐式链接 compile + link + ld
load time linking
在编译和链接阶段已经将动态库的依赖信息写入到可执行文件中,运行时,操作系统的动态链接器会自动加载这些库并解析其中的符号。
程序启动时,系统加载器(例如 /lib/ld-linux.so.*
)自动读取 ELF 文件中的依赖信息,并加载所有必需的动态库。符号解析和重定位工作由动态链接器完成,开发者在代码中直接调用库函数即可
加载器如何识别依赖的动态库?链接器(通常是 ld)会在编译器把依赖的共享库的信息写入到最终生成的 ELF 文件中。这个信息主要记录在动态段 .dynamic
里,其中有一个重要的字段 DT_NEEDED
,用于列出程序运行时所需要加载
衍生方式 3 - 弱链接 weak linking
弱链接允许在编译阶段声明某个符号为“可选”的(或说“弱”),这样在运行时即使目标动态库中不存在该符号,程序也不会链接失败。开发者可以在运行时检测该符号是否为 NULL,然后决定是否调用或使用备用实现。当应用需要兼容不同版本的库,某些新版本的 API 在旧版本中可能不存在时很有用。也可以用于实现缺失时回退到默认行为或其他实现。
在 GCC 中,可以这样声明一个函数为弱符号:
extern void some_optional_function() __attribute__((weak));
int main() {
if (some_optional_function) {
some_optional_function();
} else {
// 使用备用方案
}
return 0;
}
这样,如果链接的库中没有 some_optional_function
,程序依然可以运行,并根据检测结果选择调用。
衍生方式 4 - dlmopen 加载到独立命名空间
防止名称冲突的衍生系统调用
// LM_ID_NEWLM 表示在一个新的命名空间中加载
void *handle = dlmopen(LM_ID_NEWLM, "libexample.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlmopen error: %s\n", dlerror());
return -1;
}
double (*cos_func)(double) = (double (*)(double)) dlsym(handle, "cos");
...
}
调试所有符号
#include <stdio.h>
#include <dlfcn.h>
int main() {
// 使用 RTLD_DEFAULT 搜索全局符号
void (*printf_ptr)(const char*, ...) = dlsym(RTLD_DEFAULT, "printf");
if (printf_ptr) {
printf_ptr("Found printf via RTLD_DEFAULT\n");
} else {
fprintf(stderr, "Symbol not found: %s\n", dlerror());
}
return 0;
}
补充知识
1. 动态库的创建与编译
动态库的生成步骤:
# 编译为位置无关代码(-fPIC)
gcc -c -fPIC mylib.c -o mylib.o
# 生成共享库(-shared)
gcc -shared -o libmylib.so mylib.o
# 隐式链接时,编译主程序需指定链接库路径和名称
gcc main.c -L. -lmylib -o main
-fPIC
:生成位置无关代码(Position-Independent Code),确保动态库可被加载到任意内存地址。-shared
:指示生成共享库(.so
文件)。-L.
:添加库搜索路径(此处为当前目录)。-lmylib
:链接名为libmylib.so
的库。
2. 动态库的搜索路径
系统按以下顺序查找动态库:
- 编译时指定的
-rpath
或-rpath-link
路径。 - 环境变量
LD_LIBRARY_PATH
中的路径。 /etc/ld.so.cache
中缓存的路径(通过ldconfig
更新)。- 默认路径:
/lib
,/usr/lib
,/lib64
,/usr/lib64
等。
示例:临时添加库路径:
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH
./main
3. 符号可见性控制
默认情况下,动态库会导出所有全局符号。可通过编译选项或源码属性限制符号导出,避免污染全局命名空间。
方法1:编译时隐藏符号
使用 -fvisibility=hidden
,然后显式导出需要的符号:
// 在源码中声明导出符号
__attribute__((visibility("default"))) void public_func() { ... }
方法2:版本脚本
使用链接器脚本 libmylib.version
控制符号可见性:
gcc -shared -o libmylib.so mylib.o -Wl,--version-script=libmylib.version
脚本内容:
VERS_1.0 {
global: public_func;
local: *;
};
4. 动态库的版本控制
通过 soname(Shared Object Name)管理兼容性:
# 编译时指定 soname
gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0 mylib.o
# 创建软链接供程序查找
ln -s libmylib.so.1.0 libmylib.so.1
ln -s libmylib.so.1 libmylib.so
- 主版本号(Major):
libmylib.so.1
,表示二进制兼容性。 - 次版本号(Minor):
libmylib.so.1.0
,表示新增功能但兼容。 - 发布版本(Release):可选,如
libmylib.so.1.0.0
。
5. 动态库的初始化与清理函数
可在库中定义构造函数(加载时执行)和析构函数(卸载时执行):
__attribute__((constructor)) void init() {
printf("Library loaded!\n");
}
__attribute__((destructor)) void cleanup() {
printf("Library unloaded!\n");
}
6. 调试工具
ldd
:查看程序的动态库依赖关系。ldd ./main
nm
:列出库中的符号。nm -D libmylib.so
readelf
:查看 ELF 文件详细信息。readelf -d libmylib.so # 查看动态段(DT_NEEDED 等)
LD_DEBUG
:启用动态链接器的调试输出。LD_DEBUG=files,libs ./main
7. 安全性注意事项
LD_PRELOAD
:强制优先加载指定库,可用于劫持函数(慎用)。LD_PRELOAD=/path/to/malicious.so ./main
- 符号冲突:若两个动态库导出同名符号,先加载的符号会被使用。