内容学习自《 程序员的自我修养 链接装载与库》

如果只是想知道如何使用如何加载动态库和那4个函数的使用,可以直接从如何加载动态库开始看。

介绍

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Run-time Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。从前面我们了解到的来看,如果动态链接器可以在运行时将共享模块装载进内存并且可以进行重定位等操作,那么这种运行时加载在理论上也是很容易实现的。而且一般的共享对象不需要进行任何修改就可以进行运行时装载,这种共享对象往往被叫做动态装载库(Dynamic Loading Library),其实本质上它跟一般的共享对象没什么区别,只是程序开发者使用它的角度不同。

这种运行时加载使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。当程序需要用到某个插件或者驱动的时候,才将相应的模块装载进来,而不需要从一开始就将他们全部装载进来,从而减少了程序启动时间和内存使用。并且程序可以在运行的时候重新加载某个模块,这样使得程序本身不必重新启动而实现模块的增加、删除、更新等,这对于很多需要长期运行的程序来说是很大的优势。最常见的例子是Web服务器程序,对于Web服务器程序来说,它需要根据配置来选择不同的脚本解释器、数据库连接驱动等,对于不同的脚本解释器分别做成一个独立的模块,当Web服务器需要某种脚本解释器的时候可以将其加载进来;这对于数据库连接的驱动程序也是一样的原理。另外对于一个可靠的Web服务器来说,长期的运行是必要的保证,如果我们需要增加某种脚本解释器,或者某个脚本解释器模块需要升级,则可以通知Web服务器程序重新装载该共享模块以实现相应的目的。

如何加载动态库

在Linux中,从文件本身的格式上来看,动态库实际上跟一般的共享对象没有区别,正如我们前面讨论过的。主要的区别是共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态连接器自动完成,对于程序本身是透明的;而动态库的装载则是通过一系列由动态链接器提供的API,具体地讲共有4个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>。

/* man dlopen */
NAME
dladdr, dlclose, dlerror, dlopen, dlsym, dlvsym - programming interface
to dynamic linking loader SYNOPSIS
#include <dlfcn.h> void *dlopen(const char *filename, int flag); char *dlerror(void); void *dlsym(void *handle, const char *symbol); int dlclose(void *handle); Link with -ldl. // -ldl 表示DL库 /lib/libdl.so.2 您的环境可能是高版本的dl库。

dlopen()

dlopen() 函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程。

它的C原型定义为:

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

第一个参数: 被加载动态库的路径

  • 如果这个路径是绝对路径(以“/”开始的路径),则该函数将会尝试直接打开该动态库;
  • 如果是相对路径,那么dlopen()会尝试在以一定的顺序去查找该动态库文件:
    1. 查找有环境变量LD_LIBRARY_PATH指定的一系列目录(我们在后面会详细介绍LD_LIBRARY_PATH环境变量)。

    2. 查找由/etc/ld.so.cache里面所指定的共享库路径。

    3. /lib、/usr/lib 注意:这个查找顺序与旧的a.out装载器的顺序刚好相反,旧的a.out的装载器在装载共享库的时候会先查找/usr/lib,然后是/lib。

当然,这在理论上不应该成为一个问题,因为所有的库都应该只存在于某个目录中,而不应该在多个目录有不同的副本,这将会导致系统变得极为不可靠。

很有意思的是,如果我们将filename这个参数设置为0,那么dlopen返回的将是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们,这有些类似高级语言反射(Reflection)的特性。全局符号表包括了程序的可执行文件本身、被动态链接器加载到进程中的所有共享模块以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。

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

  • 常量RTLD_LAZY表示使用延迟绑定,当函数第一次被用到时才进行绑定,即PLT机制;

  • RTLD_NOW表示当模块被加载时即完成所有的函数绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,那么dlopen()就返回错误。

    上面的两种绑定方式必须选其一。

  • 另外还有一个常量RTLD_GLOBAL可以跟上面的两者中任意一个一起使用(通过常量的“或”操作),它表示将被加载的模块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号。

在调试程序的时候我们可以使用RTLD_NOW作为加载参数,因为如果模块加载时有任何符号未被绑定的话,我们可以使用dlerror()立即捕获到相应的错误信息;而如果使用RTLD_LAZY的话,这种符号未绑定的错误会在加载后发生,则难以捕获。当然,使用RTLD_NOW会导致加载动态库的速度变慢。

dlopen 返回值

  • lopen的返回值是被加载的模块的句柄,这个句柄在后面使用dlsym或者dlclose时需要用到。
  • 如果加载模块失败,则返回NULL。
  • 如果模块已经通过dlopen被加载过了,那么返回的是同一个句柄。

另外如果被加载的模块之间有依赖关系,比如模块A依赖与模块B,那么程序员需要手工加载被依赖的模块,比如先加载B,再加载A。

事实上dlopen还会在加载模块时执行模块中初始化部分的代码,我们前面提到过,动态链接器在加载模块时,会执行“.init”段的代码,用以完成模块的初始化工作,dlopen的加载过程基本跟动态链接器一致,在完成装载、映射和重定位以后,就会执行“.init”段的代码然后返回。

dlsym()

dlsym()函数基本上是运行时装载的核心部分,我们可以通过这个函数找到所需要的符号。

它的C原型定义为:

void *dlsym(void *handle, const char *symbol);
void *dlsym(void *handle, char *symbol); // 老版本

参数:

  • 第一个参数是由dlopen()返回的动态库的句柄;
  • 第二个参数即所要查找的符号的名字,一个以“\0”结尾的C字符串。

返回值

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

dlsym()返回的值对于不同类型的符号,意义是不同的。

  • 如果查找的符号是个函数,那么它返回函数的地址;
  • 如果是个变量,它返回变量的地址;
  • 如果这个符号是个常量,那么它返回的是该常量的值。

这里有一个问题是:如果常量的值刚好是NULL或者0呢,我们如何判断dlsym()是否找到了该符号呢?

这就要用到我们下面介绍的dlerror()函数了。如果符号找到了,那么dlerror()返回NULL,如果没找到,dlerror()就会返回相应的错误信息。

符号不仅仅是函数和变量,有时还是常量,比如表示编译单元文件名的符号等,这一般由编译器和链接器产生,而且对外不可见,但它们的确存在于模块的符号表中。dlsym()是可以查找到这些符号的,我们也可以通过“objdump –t”来查看符号表,常量在符号表里面的类型是“*ABS*”。

符号优先级

前面在介绍动态链接实现时,我们已经碰到过许多共享模块中符号名冲突的问题,结论是当多个同名符号冲突时,先装入的符号优先,我们把这种优先级方式称为装载序列(Load Ordering)。那么当我们的进程中有模块是通过dlopen()装入的共享对象时,这些后装入的模块中的符号可能会跟先前已经装入了的模块之间的符号重复。那么这时候模块之间的符号冲突该怎么解决呢?实际上不管是之前由动态链接器装入的还是之后由dlopen装入的共享对象,动态链接器在进行符号的解析以及重定位时,都是采用装载序列。

那么当我们使用dlsym()进行符号的地址查找工作时,这个函数是不是也是按照装载序列的优先级进行符号的查找呢?实际的情况是,dlsym()对符号的查找优先级分两种类型。第一种情况是,如果我们是在全局符号表中进行符号查找,即dlopen()时,参数filename为NULL,那么由于全局符号表使用的装载序列,所以dlsym()使用的也是装载序列。第二种情况是如果我们是对某个通过dlopen()打开的共享对象进行符号查找的话,那么采用的是一种叫做依赖序列(Dependency Ordering)的优先级。什么叫依赖序列呢?它是以被dlopen()打开的那个共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。

dlerror()

它的C原型定义为:

char *dlerror(void);

每次我们调用dlopen()、dlsym()或dlclose()以后,我们都可以调用dlerror()函数来判断上一次调用是否成功

dlerror()的返回值类型是char*,如果返回NULL,则表示上一次调用成功;如果不是,则返回相应的错误消息。

dlclose()

dlclose()的作用跟dlopen()刚好相反,它的作用是将一个已经加载的模块卸载。

它的C原型定义为:

int dlclose(void *handle);

系统会维持一个加载引用计数器,每次使用dlopen()加载某模块时,相应的计数器加一;每当使用dlclose()卸载某模块时,相应的计数器减一。只有当计数器值减到0时,模块才被真正的卸载掉。

卸载的过程和加载正好相反,先执行 ".finit"段的代码,然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件。

show code

#include <stdio.h>
#include <dlfcn.h> // dlfcn.h int main(int argc, char *argv[])
{
void *handle;
char *error; double (*func)(double); // 函数指针 handle = dlopen(argv[1], RTLD_NOW); // 在执行的时候需要在终端指定一个动态库的path
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_runso;
} printf("%f\n", func(3.1415926 / 2)); exit_runso:
dlclose(handle);
return 0;
} // $gcc –o RunSoSimple RunSoSimple.c –ldl
// $./RunSoSimple /lib/libm-2.6.1.so
// 1.000000

注意: -ldl 表示使用DL库(Dynamical Loading),它位于/lib/libdl.so.2。您的环境可能是高版本的dl库。

Linux下显示运行时链接(运行时加载)的更多相关文章

  1. linux下添加动态链接库路径、动态库加载等方法

    linux下添加动态链接库路径的方法 2017年01月20日 10:08:17 阅读数:5596   Linux共享库路径配置 Linux下找不到共享库文件的典型现象为明明已经安装某个软包(如libn ...

  2. [Nginx] 在Linux下的启动、停止和重加载

    Nginx的启动 /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf 其中-c参数指定配置文件路径.   Nginx的停止 ...

  3. Linux下 SpringBoot jar项目后台运行、查看、停用

    运行java jar: nohup java -jar **-0.0.1-SNAPSHOT.jar & 查看进程: 采用top或者ps aux命令.一般 如果后台是springboot,jar ...

  4. Linux下librdkafka客户端的编译运行

    Linux下librdkafka客户端的编译运行 librdkafka是一个开源的Kafka客户端C/C++实现,提供了Kafka生产者.消费者接口. 由于项目需要,我要将Kafka生产者接口封装起来 ...

  5. 使用Mac下的sequel Pro链接数据库时提示错误(已解决)

    使用Mac下的sequel Pro链接数据库时,出现如下问题: ? 1 MySQL said: Authentication plugin 'caching_sha2_password' cannot ...

  6. linux下软、硬链接的创建和删除

    linux下软.硬链接的创建和删除 在Linux系统中,内核为每一个新创建的文件分配一个Inode(索引结点),每个文件都有一个惟一的inode号.文件属性保存在索引结点里,在访问文件时,索引结点被复 ...

  7. [转帖]Linux 下软链接和硬链接的区别

    Linux 下软链接和硬链接的区别 http://os.51cto.com/art/201911/605267.htm 软连接 文件是小的 只是一个链接 删除和其他处理不影响 原始文件的计数 删除源文 ...

  8. linux下显示dd命令的进度:

    linux下显示dd命令的进度: dd if=/dev/zero of=/tmp/zero.img bs=10M count=100000 想要查看上面的dd命令的执行进度,可以使用下面几种方法: 比 ...

  9. linux下显示dd命令的进度

    sudo dd if=/dev/zero of=/tmp/zero.img status=progresslinux下显示dd命令的进度:dd if=/dev/zero of=/tmp/zero.im ...

随机推荐

  1. 【Azure SQL】数据库性能分析

    前置条件 用户有查询数据统计权限 GRANT VIEW DATABASE STATE TO database_user; CPU性能问题 正在发生 查看前X个CPU消耗查询 (汇总) SELECT T ...

  2. cocos2dx Mac平台 打印长字符串,游戏卡死

    1,打开了输出控制台,输出卡死的解决方案: 打开控制台: game -console enable 关闭控制台: game -console false 修改文件 ConsoleWindowContr ...

  3. Java 多线程基础(八)线程让步

    Java 多线程基础(八)线程让步 yield 一.yield 介绍 yield()的作用是让步.它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权:但是,并 ...

  4. Android Studio出现:Cause: unable to find valid certification path to requested target问题解决

    Android Studio , Flutter , IDEA 工程报错 unable to find valid certification path to requested target 最新解 ...

  5. CODING DevOps 系列第五课:微服务测试——微服务下展开体系化的微服务测试

    微服务测试的痛点与挑战 这张图可以形象地展示单体服务和微服务的对比,单体应用就像左边巨大的集装箱,软件模块和应用都包括其中:而微服务就像是由一个小集装箱组成,微小的服务组成一个庞大.完整的系统.单体服 ...

  6. elasticsearchBouncing Results问题

    bouncing results问题,两个document排序,field值相同:不同的shard上,可能排序不同:每次请求轮询打到不同的replica shard上:每次页面上看到的搜索结果的排序都 ...

  7. JavaWeb网上图书商城完整项目--BaseServlet

    1.以前进行操作的时候,例如我们进行登陆操作我们使用LoginServlet进行处理,进行注册操作我们使用RegisterServlet,很多业务的操作的时候我们就要定义很多个Servlet 有了Ba ...

  8. java 加密与解密艺术二

    首先需要明确的是RSA的密钥对不能手动指定,需要通过代码系统生成 接下来我们来介绍下生成密钥对 package com.weiyuan.test; import java.security.KeyPa ...

  9. springmvc-实现增删改查

    30. 尚硅谷_佟刚_SpringMVC_RESTRUL_CRUD_显示所有员工信息.avi现在需要使用restful风格实现增删改查,需要将post风格的请求转换成PUT 请求和DELETE 请求 ...

  10. redis基础二----操作set数据类型

    set集合是无序的,不能存在重复元素 bbb吃重复元素,是不能添加成功的 2 接下来分析zset,是有序的,你在添加的时候要指定元素的序列号 上面的 3 4 5 6 就是指定的元素的序列号 withs ...