Linux动态链接4:运行时链接

最近打算抽空学习张绍文老师的《Android开发高手课》。
想要彻底理解本地监控APP内存的框架的实现原理。
发现理解起来都没有那么容易,在阅读代码的过程中,发现C++、linux、native hook、framework等方面的功底均有所不足。
张绍文老师说过:“看再多的文章,不去思考文章所讲的内容和意图也是没用的;思考再多,不去动手真正实践也是没用的。”
“把进阶的各个主题由点到线串联起来,但这背后必然少不了一些基础的、底层的知识进行支撑”。
这里就把空缺的知识进行补足。

显式运行时链接

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Run-time Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。

从前面我们了解到的来看,如果动态链接器可以在运行时将共享模块装载进内存并且可以进行重定位等操作,那么这种运行时加载在理论上也是很容易实现的。

而且一般的共享对象不需要进行任何修改就可以进行运行时装载,这种共享对象往往被叫做动态装载库 (Dynamic Loading Library),其实本质上它跟一般的共享对象没什么区别,只是程序开发者使用它的角度不同。

这种运行时加载使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。当程序需要用到某个插件或者驱动的时候,才将相应的模块装载进来,而不需要从一开始就将他们全部装载进来,从而减少了程序启动时间和内存使用。并且程序可以在运行的时候重新加载某个模块,这样使得程序本身不必重新启动而实现模块的增加、删除、更新等,这对于很多需要长期运行的程序来说是很大的优势。

显式运行时链接举例

最常见的例子是 Web 服务器程序,对于Web 服务器程序来说,它需要根据配置来选择不同的脚本解释器、数据库连接驱动等,对于不同的脚本解释器分别做成一个独立的模块,当 Web 服器需要某种脚本解释器的时候可以将其加载进来;这对于数据库连接的驱动程序也是一样的原理。

另外对于一个可靠的 Web 服务器来说,长期的运行是必要的保证,如果我们需要增加某种脚本解释器,或者某个脚本解释器模块需要升级,则可以通知 Web 服务器程序重新装载该共享模块以实现相应的目的。

动态库和共享对象的区别

在 Linux 中,从文件本身的格式上来看,动态库实际上跟一般的共享对象没有区别
主要的区别是:

共享对象

共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态链接器自动完成,对于程序本身是透明的。

动态库

而动态库的装载则是通过一系列由动态链接器提供的 API,具体地讲共有 4 个函数:

1.打开动(dlopen)
2.查找符号(dlsym)
3.错误处理 (dlerror)
4.关闭动库 (dlclose)

程序可以通过这几个API 对动态库进行操作。这几个 API的实现是在/lib/libdl.so.2 里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>。

dlopen()

dlopen函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程它的C原型定义为:

void*dlopen (constchar*filename,int flag);

参数1

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

参数2

RTLD_LAZY

第二个参数 flag 表示函数符号的解析方式,常量 RTLD_LAZY 表示使用延迟绑定,当函数第一次被用到时才进行绑定,即 PLT 机制:

RTLD_NOW

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

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

RTLD_GLOBAL

另外还有一个常量 RTLD_GLOBAL可以跟上面的两者中任意一个一起使用(通过常量的“或”操作,它表被加的块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号。在调试程序的时候我们可以使用 RTLD_NOW 作为加载参数,

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

返回值

dlopen 的返回值是被加载的模块的句柄,这个句柄在后面使用 dlsym 或者 dlclose 时需要用到。
如果加载模块失败,则返回 NULL。如果模块已经通过 dlopen 被加载过了,那么返回的是同一个句柄。
另外如果被加载的模块之间有依赖关系,比如模块 A 依赖与模块 B。那么程序员需要手工加载被依赖的模块,比如先加载B,再加载A。

dlopen 加载模块时执行初始化代码

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

dlsym()

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

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

定义非常简洁,两个参数
第一个参数是由 dlopen(返回的动态库的句柄,
第二个参数即所要查找的符号的名字,一个以“0”结尾的 C 字符串。

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

dlsym()返回的值对于不同类型的符号,意义是不同的。如果查找的符号是个函数,那么它返回函数的地址;如果是个变量,它返回变量的地址;如果这个符号是个常量,那么它返回的是该常量的值。这里有一个问题是:如果常量的值刚好是 NULL 或者0 呢?
我们如何判断 dsym()是否找到了该符号呢?

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

dlerror()

每次我们调用 dlopen(dlsym0或 dlclose()以后,我们都可以调用 dlerror()函数来判断上一次调用是否成功。dlerror0的返回值类型是char*,如果返回NULL,则表示上一次调用成功;如果不是,则返回相应的错误消息。

dlclose()

dclose的作用跟 dlopen刚好相反,它的作用是将一个已经加载的模块卸载。
系统会维持一个加载引用计数器,每次使用 dlopen加载某模块时,相应的计数器加一.
每次使用diclose0卸载某模块时,相应计数器减一。只有当计数器值减到0时,模块才被真正地卸载掉。
卸载的过程跟加载刚好相反,先执行“.finit”段的代码,然后将相应的符号从符号表中去除,取消进程空间跟模块的映射关系,然后关闭模块文件。

参考资料 –
《程序员的自我修养一一链接、装载与库》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林树杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值