dlopen动态装载共享库

本文详细介绍了显式运行时链接的概念及其实现方法,包括dlopen、dlsym、dlerror和dlclose等核心API的使用,并提供了一个简单的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接: 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>

  1. dlopen:打开一个动态库,并将其加载到进程的地址空间,完成初始化过程

    1. 函数原型:void * dlopen(const char *filename, int flag);

    2. 参数说明:

    3. 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 方式的模块中的所有符号。

    4. 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

    5. 注意事项:

      dlopen 的返回值是被加载模块的句柄,该句柄在后续使用 dlsym() 和 dlclose() 时需要用到。如果加载失败,则返回 NULL 。

      如果模块已经被加载过了,那么将返回与上一次加载相同的句柄。

      如果加载的模块之间存在依赖关系,那么该依赖需要手动干预。比如模块 A 依赖于模块 B,那么必须先加载 B,再进行加载 A 的操作。

  2. dlsym:运行时装载的核心,通过该函数找到所需符号

    1. 函数原型:void * dlsym(void *handle, char *symbol);

    2. 参数介绍:

      1. void *handle:dlopen() 返回的动态库句柄;

      2. char *symbol:需要查找的符号名字,一个以 ‘\0’ 结尾的C字符串。

      3. 使用说明:

      如果 dlsym() 找到对应的符号,则返回该符号的值;没有找到,则返回 NULL。

      dlsym() 返回的值对于不同类型的符号,意义不同。如果查找的符号是函数,那么将返回该函数的地址;如果是变量,则返回该变量的地址;如果是常量,那么返回的是常量的值,如果常量的值恰好是 NULL 或 0,那么将通过 dlerror() 函数来进行判断,如果找到符号,那么 dlerror() 将返回 NULL;如果没找到,dlerror() 会返回对应的错误信息

      1. 注意事项:

      在查找动态库符号时,存在符号优先级问题。常规的动态链接采用的优先级方式为装载序列,即先装入的符号优先。但是 dlsym() 对符号的查找优先级存在两种情况:

      (1)如果在全局符号表中进行符号查找,即 dlopen() 的 filename 参数为 NULL,那么由于全局符号表使用的是装载序列,所以 dlsym() 使用的也是装载序列

      (2)如果是对某个通过 dlopen() 打开的共享对象进行符号查找的话,那么采用的是一种叫做依赖序列的优先级。依赖序列,是以被 dlopen() 打开的共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。

  3. dlerror

    1. 函数原型:char * dlerror (void);

    2. 使用说明:

      每次调用 dlopen()、dlsym() 或 dlclose() 以后,都可以调用 dlerror() 函数来判断上一次调用是否成功。如果成功,则 dlerror() 返回 NULL;如果失败,则返回对应的错误信息。

  4. dlclose

    1. 函数原型:int dlclose (void * handle);

    2. 使用说明:

      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]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值