支持动态链接的系统往往支持一种更加灵活的模块加载方式,即显式运行时链接,有时也称运行时加载。程序可以自己在运行的时候控制模块的装载和卸载。

这种能够随便装载和卸载的库称为动态装载库,在 Linux 中,动态库和一般的共享对象主要的区别就是共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态链接器自动完成,对于程序本身就是透明的,而动态库的装载则是通过一系列由动态链接器提供的 API 来进行操作。API 的实现在 libdl.so.2 里,声明定义在 <dlfcn.h>

动态库的装载具体讲有四个函数:

dlopen

打开一个动态库,将其加载到进程的地址空间,完成初始化操作

void * dlopen(const char* filename,int flag)

第一个参数是被加载动态库的路径,如果是动态路径则会从下面的地方进行搜索:

  1. 查找有环境变量 LD_LIBRARY_PATH 指定的一系列目录
  2. 查找由 /etc/ld.so.cache 里面指定的共享库路径
  3. /lib /usr/lib

如果将 filename 参数设置为 0,返回的将会是 全局符号表 的句柄,类似 反射特性

第二个参数表示函数符号的解析方式

  • RTLD_LAZY 表示延迟绑定,就是 PLT
  • RTLD_NOW 表示模块被加载完成之后所有的函数绑定工作,如果模块加载时有任何符号没有被绑定的话,可以用 dlerr 捕获到相应的错误信息
  • RTLD_GLOBAL 表示将加载的模块的全局符号合并到进程的全局符号中(用或连接)

函数返回加载模块句柄,失败返回 NULL

dlopen 还会在加载模块的时候执行模块中初始化部分的代码(.init 段)

dlsym

可以通过句柄找到所需要的符号

void * dlsym(void *handle, char * symbol);

第一个函数是句柄,dirge 函数是我们要查找符号的名字(C 字符串)

如果查找到符号是一个函数,那么返回的就是函数的地址;如果是一个变量,他就返回的是变量的地址,如果是一个常量,那么返回的就是这个常量的值

如果值本身就为空,那么我们就需要通过 dlerror 来判断是否找到这个符号

类似 全局符号介入 的问题,在通过 dlopen 打开对象查找符号的时候,根据它依赖的共享对象进行广度优先遍历,这种优先级称为 依赖序列 优先级

dlerror

返回值是 char* 如果返回 NULL 则表示上一次调用成功,如果不是就返回相应的错误信息

dlclose

将一个已经加载的模块卸载(有计数器),先执行 .finit 段代码,然后将相应的符号从符号表中去除,取消进程空间和模块的映射关系,然后关闭模块

#include <dlfcn.h>
#include <stdio.h>
 
int main(int argc, char **argv) {
    void *handle;
    double (*func)(double);
    char *error;
 
    handle = dlopen(argv[1], RTLD_NOW);
    if (handle == NULL) {
        printf("Open library %s error: %s\n", argv[1], dlerror());
        return -1;
    }
    func = dlsym(handle, "sin");
    if ((error = dlerror()) != NULL) {
        printf("Symbol sin not found: %s\n", error);
        goto exit;
        return -1;
    }
    printf("%f\n", func(3.14 / 2));
 
exit:
    dlclose(handle);
    return 0;
}
➜  c gcc -o run_so_simple main.c -ldl
➜  c ./run_so_simple /lib/x86_64-linux-gnu/libm.so.6 
1.000000!