原文链接: http://www.ccccxy.top/coding/archives/2020/10/01/dlopen_load_shared_library_11/
欢迎大神们评论指导和斧正
一、基本说明
显式运行时链接 (Explicit Run-time Linking),有时候也叫做运行时加载,程序自己在运行时控制加载指定的模块,即动态库,并且可以在不需要该模块时将其卸载。
和普通的动态链接相比,显式运行时链接,可以使程序在需要使用到某个插件或驱动时,才将相应的模块加载进来,而不需要在一开始就将所有插件和驱动的模块进行加载,从未减少了程序启动的时间和内存使用。并且程序可以在运行过程中动态的重新加载模块,从而实现程序在不重启的前提下,实现模块的增加、删除、更新等需求。
普通的动态链接,是由动态链接器在程序启动之前完成模块的装载和链接的,这一过程自动完成(eg:gcc -o demo demo.c -ltest
);但是动态库的显式运行链接则是通过一系列的动态链接器提供的API(dlopen,dlsym,dlerror,dlclose),手动干预完成。
二、接口介绍
运行时加载,一共由四个函数完成:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)、卸载动态库(dlclose)。这四个API实现在/lib/libdl.so.2
中,声明和相关常量被定义在系统标准头文件<dlfcn.h>
。
-
dlopen:打开一个动态库,并将其加载到进程的地址空间,完成初始化过程
-
函数原型:
void * dlopen(const char *filename, int flag);
-
参数说明:
-
const char *filename:被加载的动态库的路径
该路径可以是绝对路径,如果使用了绝对路径,那么 dlopen 函数会尝试直接打开对应路径的动态库;也可以使用相对路径,在该情况下,dlopen 则会尝试按一定顺序去寻找该动态库,若找到则打开,并返回对应句柄。
以下为使用相对路径情况下,dlopen 函数查找的顺序:
(1)(仅限ELF)如果程序的可执行文件中包含 DT_RPATH 标签并且没有包含 DT_RUNPATH 标签,那么查找 DT_RPATH 中指定的目录
(2)查找环境变量 LD_LIBRARY_PATH 指定的一系列目录
(3)(仅限ELF)如果可执行文件中包含了 DT_RUNPATH 标签,那么查找 DT_RUNPATH 中指定的目录
(4)查找由 /etc/ld.so.cache 里面所指定的共享库路径
(5)/lib 、/usr/lib
(1和3,来自于Linux Programmer’s Manual,也有人说这里的描述是错误的,正确性尚未明确)
filename 还可以被设置为0,那么 dlopen 返回的将是全局符号表的句柄,也就是说,可以在运行时找到全局符号表里的任何一个符号,并且可以执行他们。全局符号表包括了:程序可执行文件本身、被动态链接库加载到进程中的所有共享模块,以及在运行时通过 dlopen() 打开并且使用了 RTLD_GLOBAL 方式的模块中的所有符号。
-
int flag:函数符号的解析方式,有以下几种,部分方式在使用时可以通过“或”运算一起使用
-
RTLD_LAZY:使用延迟绑定。
当函数在第一次被调用的时候才执行绑定(符号查找、重定位等),如果没有用到则不进行绑定。所以在 dlopen() 函数返回的时候,模块间调用的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定,这样能提高模块的加载速度。
-
RTLD_NOW:
当模块被加载完时即完成所有函数的绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,那么 dlopen() 就会返回错误。
-
RTLD_GLOBAL:
被加载的模块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号。
-
RTLD_LOCAL:
与 RTLD_GLOBAL 作用相反,该动态库内定义的符号不能被在其后加载的动态库重定位,即后续加载的动态库不可使用该库中的符号。
注:RTLD_LAZY 和 RTLD_NOW 必须使用一个;RTLD_GLOBAL 和 RTLD_LOCLA 可以与 RTLD_LAZY 和 RTLD_NOW 任意一个一起使用,通常操作为“或“,RTLD_LOCLA 和 RTLD_GLOBAL 在缺省的情况下,默认为 RTLD_LOCLA
-
-
注意事项:
dlopen 的返回值是被加载模块的句柄,该句柄在后续使用 dlsym() 和 dlclose() 时需要用到。如果加载失败,则返回 NULL 。
如果模块已经被加载过了,那么将返回与上一次加载相同的句柄。
如果加载的模块之间存在依赖关系,那么该依赖需要手动干预。比如模块 A 依赖于模块 B,那么必须先加载 B,再进行加载 A 的操作。
-
-
dlsym:运行时装载的核心,通过该函数找到所需符号
-
函数原型:
void * dlsym(void *handle, char *symbol);
-
参数介绍:
-
void *handle:dlopen() 返回的动态库句柄;
-
char *symbol:需要查找的符号名字,一个以 ‘\0’ 结尾的C字符串。
-
使用说明:
如果 dlsym() 找到对应的符号,则返回该符号的值;没有找到,则返回 NULL。
dlsym() 返回的值对于不同类型的符号,意义不同。如果查找的符号是函数,那么将返回该函数的地址;如果是变量,则返回该变量的地址;如果是常量,那么返回的是常量的值,如果常量的值恰好是 NULL 或 0,那么将通过 dlerror() 函数来进行判断,如果找到符号,那么 dlerror() 将返回 NULL;如果没找到,dlerror() 会返回对应的错误信息。
- 注意事项:
在查找动态库符号时,存在符号优先级问题。常规的动态链接采用的优先级方式为装载序列,即先装入的符号优先。但是 dlsym() 对符号的查找优先级存在两种情况:
(1)如果在全局符号表中进行符号查找,即 dlopen() 的 filename 参数为 NULL,那么由于全局符号表使用的是装载序列,所以 dlsym() 使用的也是装载序列;
(2)如果是对某个通过 dlopen() 打开的共享对象进行符号查找的话,那么采用的是一种叫做依赖序列的优先级。依赖序列,是以被 dlopen() 打开的共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。
-
-
-
dlerror
-
函数原型:
char * dlerror (void);
-
使用说明:
每次调用 dlopen()、dlsym() 或 dlclose() 以后,都可以调用 dlerror() 函数来判断上一次调用是否成功。如果成功,则 dlerror() 返回 NULL;如果失败,则返回对应的错误信息。
-
-
dlclose
-
函数原型:
int dlclose (void * handle);
-
使用说明:
dlclose() 的作用与 dlopen() 相反,它的作用是将一个已经加载的模块卸载。系统会维持一个加载引用计数器,每次使用 dlopen() 加载某模块时,相应的计数器加1;每次使用 dlclose() 卸载某模块时,相应计数器减1。只有当计数器减至0时,模块才被真正地卸载掉。卸载过程与加载相反,先执行“.finit”段代码,然后将相应的符号从符号表中去除,取消进程空间与模块的映射关系,然后关闭模块文件。
-
附录
以下为dlopen等接口的简单使用demo:
//"a.h"
//libadd.so头文件
#ifndef __A_H__
#define __A_H__
#ifdef __cplusplus
extern "C"{
#endif
int add(int x, int y);
#ifdef __cplusplus
}
#endif
#endif // !__A_H__
//a.c
//libadd.so的实现,简单实现两个int类型整数的相加
#include "a.h"
int add(int x, int y)
{
return x+y;
}
//cmd: gcc -fpic -shared -o libadd.so -g a.c
//main.c
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
typedef int(*func_add)(int, int);
//相对路径
const char* relativePath_a = "./libadd.so";
//绝对路径
const char* absPath_a = "/home/44124/test/libadd.so";
int main(int argc, char** argv)
{
void* handle = dlopen(absPath_a, RTLD_LAZY);
if (!handle){
fprintf(stderr, "[%s](%d) dlopen get error: %s\n", __FILE__, __LINE__, dlerror());
exit(EXIT_FAILURE);
}
do{
func_add add = (func_add)dlsym(handle, "add");
printf("1 add 2 is [%d]\n", add(1, 2));
}while(0);
dlclose(handle);
return 0;
}
//cmd: gcc -o demo -g main.c -ldl; ./demo
//output: 1 add 2 is [3]