理解 dlopen manual
内容来源:man dlopen
函数原型:
#include <dlfcn.h>
void *dlopen(const char *filename, int flags);
int dlclose(void *handle);
#define _GNU_SOURCE
#include <dlfcn.h>
void *dlmopen(Lmid_t lmid, const char *filename, int flags);
Link with -ldl.
dlopen 函数
dlopen 函数加载以 null 结尾的字符串指定的动态共享对象文件(共享库)并返回一个标识已经加载对象的 “handle” 句柄。这个句柄可以被 dlopen API 中的其它函数使用,如 dlsym、dladdr、dlinfo 与 dlclose 等函数。
当 filename 为空时,dlopen 将会返回主程序的句柄。如果 filename 中包含一个斜杠,它将被作为路径名使用。否则动态链接器使用如下顺序寻找对象:
- 仅当使用 ELF 格式时,如果调用对象(调用 dlopen 的共享库获可执行文件)包含一个 DT_RPATH 标识并且不包含 DT_RUNPATH 标识时,DT_RPATH 标识中的目录列表被检索。
- 如果程序已经启动, LD_LIBRARY_PATH 环境变量的值为以冒号分隔的目录位置,在这些目录中查找。 (出于安全性考虑,设置了 suid 与 sgid 的程序将会忽略此变量)
- 仅当使用 ELF 格式时,如果调用对象包含一个 DT_RUNPATH 标识,此标识中的目录列表被检索。
- 在 /etc/ld.so.cache 缓存文件中检索是否存在一个以 filename 标识的项目。
- 从 /lib 目录与 /usr/lib 目录检索。(先 /lib 后 /usr/lib)。
当 filename 指定的对象依赖其它的共享对象时,这些被依赖的对象也将会被动态库链接器使用相同的检索规则自动加载。(如果这些依赖对象也有依赖时,这一过程将会递归进行)
dlopen 函数的 flags 参数中必须包含如下两个值中的一个:
-
RTLD_LAZY
执行惰性绑定。仅当引用符号的代码执行时才进行符号解析与绑定。如果符号从不被引用,它将不会被解析与绑定。(惰性绑定只用于函数引用,引用变量的场景会在共享对象加载时立刻绑定)glibc 2.1.1 及更高的版本中, LD_BIND_NOW 环境变量的设置会让此标志失效。
-
RTLD_NOE
如果设定了这个标志、LD_BIND_NOW 环境变量值被设定为非空字符串,共享库对象中所有未定义的符号会在 dlopen 函数返回前被解析并完成绑定。如果不能成功完成绑定, dlopen 将会返回错误值。
0 个或多个如下值也可以在 flags 参数中被设定:
-
RTD_GLOBAL
此共享对象中定义的符号能够被后续加载的共享对象解析使用。
-
RTLD_LOCAL
与 RTLD_GLOBAL 功能相反,当这两个标志都为设定时,默认配置 RTLD_LOCAL。此共享对象中定义的符号不能被后续加载的共享对象解析使用。
-
RTLD_NODELETE(glibc 2.2 及其之后的版本)
在 dlclose 函数调用的时候不卸载共享对象。因此,此对象的静态变量与全局变量在后此共享对象被 dlopen 重新加载时不会被重新初始化。
-
RTLD_NOLOAD(glibc 2.2 及其之后的版本)
不加载共享对象。可用于测试是否对象已经被加载(dlopen 返回空表示未加载,否则返回已经加载的对象句柄)。也可用于覆盖一个已经被加载的共享对象的 flag 属性。例如一个之前以 RTLD_LOCAL 模式加载的共享对象可以被以 RTLD_NOLOAD 与 RTLD_GLOBAL flag 重新打开。
-
RTLD_DEEPBIND(glibc 2.3.4 及其之后的版本)
将当前共享对象中所有符号的查找范围放在全局范围之前。它意味着一个独立的对象将会优先使用自己内部的符号而非使用已经加载的对象中包含的同名全局符号。
当 filename 为空时,dlopen 将会返回主程序的 handle 句柄。当该句柄被传递给 dlsym 函数,符号将会依次在主程序、程序启动时加载的所有共享对象、使用 RTLD_GLOBAL 标志加载的所有共享对象中检索。
共享对象中符号引用的解析按照如下顺序进行:
- 主程序及其依赖的对象加载生成的 link map 中的符号
- 指定 RTLD_GLOBAL 标志使用 dlopen 打开的共享对象及其依赖对象中的符号
- 此共享对象自己包含的符号定义及其任何被加载的依赖对象中的符号
可执行文件中任何被 ld 链接器放入动态符号表全局符号可以在一个动态加载的共享库中被解析使用。
在如下两种情况中,符号均会被放入到动态符号表中。
- 可执行文件链接时使用了 “-rdynamic” 参数(或 “—export-dynamic” 参数)
- ld 链接器在静态链接时发现对符号的依赖在另外一个对象中
当相同的共享对象被 dlopen 再次加载时,相同的对象句柄被返回。动态库链接器管理一个对象句柄的引用计数,因此此动态加载的共享对象在 dlclos 被调用的次数与 dlopen 被成功调用的次数一样多之前不会被释放。
构造函数只在对象真正被加载进内存的时候才调用(仅当引用计数增加到 1 时)。
一个使用 RTLD_NOW flag 调用 dlopen 加载相同共享对象的子执行序列将会让早期以 RTLD_LAZY flag 加载的动态库强制立刻执行符号解析与绑定。
类似地,以前用 RTLD_LOCAL 打开的对象可以在后续的 dlopen 中切换为 RTLD_GLOBAL。
dlmopen 函数
dlmopen 函数与 dlopen 功能相同,filenname 与 flags 参数与返回值的含义都一致,主要区别如下:
dlmopen 函数接收一个额外的参数——lmid 来指定共享对象应该被加载到的 link-map 表(也称为一个 namespace)。与此相反,dlopen 动态加载共享对象到与 dlopen 调用对象相同的 namespace。
lmid 参数可以是一个已经存在的 namespace 的 id(可以使用 dlinfo 函数的 RTLD_DI_LMID 请求获取)或者如下特殊值:
-
LM_ID_BASE
将共享对象加载到初始化 namespace 中。(如应用程序的 namespace)
-
LM_ID_NEWLM
创建一个新的 namespace 并将共享对象加载到此 namespace 中。由于新的 namespace 初值为空,此对象必须正确链接所有它需要的共享对象。
如果 filename 为空,lmid 仅仅被允许设定位 LM_ID_BASE。
dlclose 函数
dlcose 函数减小一个由 handle 句柄标识的已经被加载的动态对象的引用计数。当对象的引用计数降为 0 且没有其它对象依赖本对象中的符号时,该对象在调用为此对象定义的所有析构函数后被卸载(当共享对象使用 RTLD_GLOBAL flag 打开并且它的某些符号被其它对象解析使用时,这个对象中的符号就被其它对象依赖)。
所有在 dlopen 函数被调用时自动加载的共享对象也递归地按照相同的过程卸载。
dlclose 成功返回并不保证 handle 标识的符号相关资源已经从调用者的地址空间中被移除。除了显式调用 dlopen 增加引用技术外,共享对象可能因为其它共享对象中的依赖项被隐式加载(并增加计数引用)。只有当所有引用都已释放时,才能从地址空间中删除共享对象。
注意事项
dlmopen 和命名空间
一个 link-map list 定义了用于动态链接器解析符号的独立名称空间。在一个命名空间中,依赖的共享对象根据一般规则隐式加载,并且符号引用也根据一般规则进行解析,但是这种解析仅限于已(显式和隐式)加载到命名空间中的对象提供的定义。
dlmopen 函数支持对象加载隔离以在新命名空间中加载共享对象符号,这些符号将不会暴露给应用程序的其它部分。
使用 RTLD_LOCAL 标志不能实现此功能,因为它仅能阻止共享对象的符号对任何其它共享对象可见。
在某些情况下,我们可能希望将动态加载的共享对象的符号用于其它共享对象(其子集),而向整个应用程序暴露这些符号。这可以通过使用单独的命名空间并使用 RTLD_ GLOBAL 标志来实现。
dlmopen 函数还可以用于提供比 RTLD_LOCAL 标志更好的隔离功能。对那些使用 RTLD_LOCAL flag 加载的共享对象,如果它们是其它以 RTLD_GLOBAL 标志加载的共享对象的依赖,RTLD_LOCAL 将会切换为 RTLD_GLOBAL。因此,RTLD_LOCAL 不足以隔离加载的共享对象,除非在显式控制所有共享对象依赖项(不常见)的情况下使用。
dlmopen 的可能用途是插件加载框架的作者无法信任插件作者,并且不希望任何插件框架中的未定义符号被解析为插件中的符号。另一个用途是多次加载同一对象。不使用 dlmopen 时,需要创建共享对象文件的不同副本。使用 dlmopen ,可以通过将同一共享对象文件加载到不同的命名空间实现。
glibc实现最多支持16个命名空间。
初始化与解初始化函数
共享对象可以使用 attribute((constructor)) 与 attribute((destructor)) 函数属性导出函数。 constructor 函数在 dlopen 返回之前执行,析构函数在 dlclose 返回之前执行。共享对象可以导出多个 constructor 和 destructor 函数,并设置优先级以确定它们的执行顺序。
此种功能的一种古老的实现方案是通过链接器识别的两个特殊符号:_init 和 _fini。如果动态加载的共享对象导出了名为 init 的函数,在加载共享对象之后, dlopen 在返回前执行该代码。如果共享对象导出了名为 _fini 的函数,则在卸载对象之前调用该函数。
在这种实现下,必须避免链接到包含这些文件的默认版本的系统启动文件;这可以通过使用 gcc -nostart 命令行完成。
现在 _init 和 _fini 的使用已经被废弃,取而代之的是前面提到的构造函数和析构函数,它们的优点之一是允许定义多个 constructor 与 destructor 函数。
从 glibc 2.2.3 版本开始,atexit 可以用于注册一个退出处理程序,当卸载共享对象时,会自动调用该处理程序。