跳到主要内容Linux 动态链接库使用详解:dlopen/dlsym/dlclose/dlerror | 极客日志C
Linux 动态链接库使用详解:dlopen/dlsym/dlclose/dlerror
Linux 下通过 dlopen 系列 API 实现动态链接库加载。核心包括打开库、获取符号地址、关闭库及错误处理。配合 RTLD 标志位控制解析时机与作用域,支持运行时模块扩展。编译需添加 -ldl 等参数,适用于 Apache 等服务器插件场景。
Linux 动态链接库使用详解
在 Linux 环境下,动态链接库提供了极大的灵活性。它允许程序在运行时加载模块,而不需要重新编译主程序。Apache Web 服务器就是利用这种机制在运行过程中加载模块的典型例子,这使得添加或删除功能时非常便捷。
核心 API 概览
要使用动态链接库,首先需要在代码中包含 <dlfcn.h> 头文件,并在编译时链接 -ldl 库。
dlopen:打开库
void *dlopen(const char *pathname, int mode);
这个函数负责将指定的动态库文件装入内存并返回一个句柄(handle)。
- pathname:库文件的路径,可以是绝对路径或相对路径。
- mode:控制符号解析和可见性的标志位组合。
返回值:成功返回库引用句柄,失败返回 NULL。
dlsym:获取符号地址
void *dlsym(void *handle, const char *symbol);
拿到 dlopen 返回的句柄后,用 dlsym 去查找具体的函数或变量地址。返回值是 void*,通常需要强制转换为对应的函数指针类型才能调用。
dlclose:关闭库
int dlclose(void *handle);
用于卸载之前打开的动态库。只有当该库的使用计数降为 0 时,系统才会真正将其从内存中移除。
dlerror:错误处理
const char *dlerror(void);
当上述操作失败时,dlerror 会返回具体的错误信息字符串。注意,每次调用 dlerror 后会清空之前的错误状态,所以如果 dlopen 失败了,紧接着调用 dlerror 获取原因通常是最稳妥的做法。
关键标志位说明
dlopen 的 mode 参数决定了库的行为,常见的标志包括:
- RTLD_LAZY:暂缓决定。在
dlopen 返回前,不立即解析未定义的符号,等到真正调用时才解析。这对变量引用总是立即解析,仅对函数引用有效。
RTLD_NOW:立即决定。在 dlopen 返回前,解析所有未定义符号。如果解析失败(如找不到符号),直接返回 NULL。RTLD_GLOBAL:允许导出符号。动态库中定义的符号可以被后续加载的其他库重定位使用。RTLD_LOCAL:默认行为。动态库中的符号不能被后续加载的库重定位。RTLD_NODELETE:非 POSIX 标准。在 dlclose 期间不卸载库,即使引用计数为 0 也保留在内存中。RTLD_NOLOAD:非 POSIX 标准。不加载库,主要用于测试库是否已经加载,或者修改已加载库的标志位。RTLD_DEEPBIND:非 POSIX 标准。搜索全局符号前先搜索库内符号,避免同名冲突。实战示例
基础函数调用
下面是一个简单的例子,演示如何加载包含加减乘除函数的库并调用它们。
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#define LIB_PATH "./libcaculate.so"
typedef int (*CAC_FUNC)(int, int);
int main() {
void *handle;
char *error;
CAC_FUNC cac_func = NULL;
handle = dlopen(LIB_PATH, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
dlerror();
cac_func = (CAC_FUNC)dlsym(handle, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
printf("add: %d\n", (*cac_func)(2, 7));
cac_func = (CAC_FUNC)dlsym(handle, "sub");
printf("sub: %d\n", cac_func(9, 2));
cac_func = (CAC_FUNC)dlsym(handle, "mul");
printf("mul: %d\n", cac_func(3, 2));
cac_func = (CAC_FUNC)dlsym(handle, "div");
printf("div: %d\n", cac_func(8, 2));
dlclose(handle);
return 0;
}
编译命令:gcc -rdynamic -o foo foo.c -ldl
结构体与回调交互
有时候我们需要动态库和主程序之间进行更复杂的交互,比如传递结构体指针或回调函数。
#include <stdlib.h>
#include <dlfcn.h>
#include <stdio.h>
typedef struct __test {
int i;
void (*echo_fun)(struct __test *p);
} Test;
void __register(Test *p) {
p->i = 1;
p->echo_fun(p);
}
int main(void) {
void *handle = NULL;
char *myso = "./mylib.so";
if ((handle = dlopen(myso, RTLD_NOW)) == NULL) {
printf("dlopen - %s\n", dlerror());
exit(-1);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
typedef struct __test {
int i;
void (*echo_fun)(struct __test *p);
} Test;
extern void __register(Test *p);
static void __printf(Test *p) {
printf("i = %d\n", p->i);
}
static Test config = { .i = 0, .echo_fun = __printf };
void _init(void) {
printf("init\n");
__register(&config);
}
编译动态库:gcc -shared -fPIC -nostartfiles -o mylib.so mylib.c
编译主程序:gcc test.c -ldl -rdynamic
在这个例子中,主程序通过 dlopen 加载 .so 文件,动态库会自动运行 _init() 初始化函数。这展示了主程序和动态库如何通过函数指针相互调用。
接口封装示例
对于大型库,我们通常会定义一个全局结构体来封装多个函数指针,这样就不需要多次调用 dlsym。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
typedef struct {
const char *module;
int (*GetValue)(char *pszVal);
int (*PrintfHello)();
} hello_ST_API;
int GetValue(char *pszVal) {
int retval = -1;
if (pszVal)
retval = sprintf(pszVal, "%s", "123456");
printf("%s, %d, pszVer = %s\n", __FUNCTION__, __LINE__, pszVal);
return retval;
}
int PrintfHello() {
int retval = -1;
printf("%s, %d, hello everyone\n", __FUNCTION__, __LINE__);
return 0;
}
const hello_ST_API Hello = {
.module = "hello",
GetValue,
PrintfHello,
};
编译指令:gcc -shared -o hello.so hello.c
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
typedef struct {
const char *module;
int (*GetValue)(char *pszVal);
int (*PrintfHello)();
} hello_ST_API;
int main(int argc, char **argv) {
hello_ST_API *hello;
int i = 0;
void *handle;
char psValue[20] = {0};
handle = dlopen("./hello.so", RTLD_LAZY);
if (!handle) {
printf("%s,%d, NULL == handle\n", __FUNCTION__, __LINE__);
return -1;
}
dlerror();
hello = dlsym(handle, "Hello");
if (!hello) {
printf("%s,%d, NULL == handle\n", __FUNCTION__, __LINE__);
return -1;
}
if (hello && hello->PrintfHello)
i = hello->PrintfHello();
printf("%s, %d, i = %d\n", __FUNCTION__, __LINE__, i);
if (hello && hello->GetValue)
i = hello->GetValue(psValue);
if (hello && hello->module)
printf("%s, %d, module = %s\n", __FUNCTION__, __LINE__, hello->module);
dlclose(handle);
return 0;
}
编译指令:gcc -o test hello_dlopen.c -ldl
运行结果会显示正常的输出,这说明通过 dlsym 找到全局结构体后,可以直接用指针访问库内的多个函数,比逐个查找更高效。
编译注意事项
- 创建动态库:使用
-shared 选项,并以 .so 后缀命名。建议放在 /lib 或 /usr/lib 等公用目录,或者确保程序能找到相对路径。
- 编译可执行文件:必须包含
-ldl 以链接动态加载库。同时建议使用 -rdynamic 选项,它将所有符号添加到动态符号表中,方便调试和向后跟踪。
- 位置无关代码:编译
.so 文件时通常加上 -fPIC,这样多个可执行文件可以共享同一份库代码,节省内存空间。
- 错误检查:务必检查
dlopen 和 dlsym 的返回值,并使用 dlerror 获取具体错误信息,否则很难排查问题。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online