动态加载 (DL) 库是指在程序启动以外的时间加载的库。它们对于实现插件或模块特别有用,因为它们允许延迟加载插件直到需要时才加载。例如,可插拔身份验证模块 (PAM) 系统使用 DL 库来允许管理员配置和重新配置身份验证。它们对于实现希望偶尔将其代码编译成机器代码并使用编译版本以提高效率的解释器也很有用,所有这些都无需停止。例如,这种方法在实现即时编译器或多人地下城 (MUD) 中可能很有用。
在 Linux 中,从格式的角度来看,DL 库实际上并没有什么特别之处;它们的构建方式与上面讨论的标准目标文件或标准共享库相同。主要的区别在于这些库不会在程序链接时或启动时自动加载;相反,有一个 API 用于打开库、查找符号、处理错误和关闭库。C 用户需要包含头文件 <dlfcn.h> 才能使用此 API。
Linux 使用的接口与 Solaris 中使用的接口基本相同,我将其称为“dlopen()”API。但是,并非所有平台都支持相同的接口;HP-UX 使用不同的 shl_load() 机制,而 Windows 平台使用具有完全不同接口的 DLL。如果您的目标是广泛的移植性,您可能应该考虑使用一些包装库来隐藏平台之间的差异。一种方法是 glib 库,它支持模块的动态加载;它使用平台底层的动态加载例程来实现这些函数的便携式接口。您可以在 http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html 了解有关 glib 的更多信息。由于 glib 接口在其文档中得到了很好的解释,因此我在此不再进一步讨论。另一种方法是使用 libltdl,它是 GNU libtool 的一部分。如果您需要比这更多的功能,您可能需要研究 CORBA 对象请求代理 (ORB)。如果您仍然有兴趣直接使用 Linux 和 Solaris 支持的接口,请继续阅读。
使用 C++ 和动态加载 (DL) 库的开发人员还应查阅“C++ dlopen mini-HOWTO”。
dlopen(3) 函数打开一个库并准备好供使用。在 C 语言中,它的原型是
void * dlopen(const char *filename, int flag); |
用户 LD_LIBRARY_PATH 环境变量中以冒号分隔的目录列表。
/etc/ld.so.cache 中指定的库列表(从 /etc/ld.so.conf 生成)。
/lib,后跟 /usr/lib。请注意这里的顺序;这与旧的 a.out 加载器使用的顺序相反。旧的 a.out 加载器在加载程序时,首先搜索 /usr/lib,然后搜索 /lib(请参阅手册页 ld.so(8))。这通常无关紧要,因为一个库应该只在一个或另一个目录中(永远不会同时在两个目录中),并且同名的不同库是灾难的潜在隐患。
如果库相互依赖(例如,X 依赖于 Y),则需要先加载被依赖项(在本例中,先加载 Y,然后再加载 X)。
dlopen() 的返回值是一个“句柄”,应将其视为不透明的值,供其他 DL 库例程使用。如果加载尝试不成功,dlopen() 将返回 NULL,您需要检查这一点。如果同一个库被 dlopen() 多次加载,则返回相同的文件句柄。
在较旧的系统中,如果库导出一个名为 _init 的例程,则该代码将在 dlopen() 返回之前执行。您可以在自己的库中使用此事实来实现初始化例程。但是,库不应导出名为 _init 或 _fini 的例程。这些机制已过时,可能会导致不希望的行为。相反,库应使用 __attribute__((constructor)) 和 __attribute__((destructor)) 函数属性导出例程(假设您使用的是 gcc)。有关更多信息,请参阅 第 5.2 节。
可以通过调用 dlerror() 报告错误,它返回一个字符串,描述上次调用 dlopen()、dlsym() 或 dlclose() 产生的错误。一个奇怪之处在于,在调用 dlerror() 之后,对 dlerror() 的后续调用将返回 NULL,直到遇到另一个错误。
如果您无法使用 DL 库,那么加载 DL 库就没有意义。使用 DL 库的主要例程是 dlsym(3),它在给定的(已打开的)库中查找符号的值。此函数定义为
void * dlsym(void *handle, char *symbol); |
如果未找到符号,dlsym() 将返回 NULL 结果。如果您知道该符号永远不可能具有 NULL 或零值,那可能没问题,但否则存在潜在的歧义:如果您得到 NULL,这是否意味着没有这样的符号,还是 NULL 是符号的值?标准的解决方案是首先调用 dlerror()(以清除可能存在的任何错误条件),然后调用 dlsym() 以请求符号,然后再次调用 dlerror() 以查看是否发生错误。代码片段如下所示
dlerror(); /* clear error code */ s = (actual_type) dlsym(handle, symbol_being_searched_for); if ((err = dlerror()) != NULL) { /* handle error, the symbol wasn't found */ } else { /* symbol found, its value is in s */ } |
dlclose() 是 dlopen() 的逆操作,它关闭一个 DL 库。dl 库维护动态文件句柄的链接计数,因此只有在对动态库成功调用 dlopen 的次数与 dlclose 的调用次数相同时,动态库才会被实际释放。因此,同一个程序多次加载同一个库不是问题。如果库被释放,则会调用其函数 _fini(如果存在于较旧的库中),但 _fini 是一种过时的机制,不应依赖。相反,库应使用 __attribute__((constructor)) 和 __attribute__((destructor)) 函数属性导出例程。有关更多信息,请参阅 第 5.2 节。注意:dlclose() 在成功时返回 0,在错误时返回非零值;一些 Linux 手册页没有提及这一点。
这是一个来自 dlopen(3) 手册页的示例。此示例加载数学库并打印 2.0 的余弦值,并且它在每个步骤都检查错误(推荐)
#include <stdlib.h> #include <stdio.h> #include <dlfcn.h> int main(int argc, char **argv) { void *handle; double (*cosine)(double); char *error; handle = dlopen ("/lib/libm.so.6", RTLD_LAZY); if (!handle) { fputs (dlerror(), stderr); exit(1); } cosine = dlsym(handle, "cos"); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } printf ("%f\n", (*cosine)(2.0)); dlclose(handle); } |
如果此程序位于名为“foo.c”的文件中,您可以使用以下命令构建该程序
gcc -o foo foo.c -ldl |