Linux动态链接之四:动态链接的步骤

动态链接的步骤基本上分为3步:
1.启动动态链接器本身
2.装载所需要的共享对象
3.重定位和初始化

1. 动态链接其ld.so自举

动态链接器入口地址即是自举代码的入口,当OS将进程控制权交给动态链接器时,动态链接器的自举代码开始执行。自举代码执行逻辑:
1.找到动态链接器自己的GOT段,GOT中第一项即是.dynamic段的偏移地址,然后找到该动态链接器本身的.dynamic段;
2.通过.dynamic段中的信息,自举代码可以获得动态链接器本身的重定位表和符号表等,即有.rel.dyn和.rel.plt,其中.rel.dyn是对数据引用的修正,修正的位置位于.got以及数据段,而.rel.plt(延迟绑定)是对函数引用的修正,所修正的位置位于.got.plt(考虑到动态链接器不和外部模块交互,故plt机制对动态链接器不存在);找到这些重定位信息后,进行重定位,然后就可以将这些全局变量和静态变量全部定位;
3.定位之后,动态链接器代码便可以开始自由使用自己的全局变量和静态变量。

动态链接器作为第一个被加载进虚拟空间的共享对象,显然不能依靠其他共享对象,此外由于动态链接器本身所需要的全局和静态变量的重定位是需要自身独立完成,这显然需要动态链接器的启动初始话部分要极为精巧,这种不依赖外部对象完成启动(如同电脑启动的系统启动程序)的启动代码都被称为自举。

2. 装载所需要的共享对象

这里的共享对象是链式加载的,主要是根据主程序文件的.got.plt表中的.dynamic段中DT_NEEDED表项指示的依赖对象元素的文件名,依次加载,显然新加载的共享对象还可能依赖别的对象,这便是涉及到广度优先加载还是深度优先加载的选择问题,一般常见的是采用广度优先加载。动态链接器图遍历装载ELF可执行文件需要的所有的.so对象,并每装载一个新的.so,则将该.so对象名下的符号表合并到全局符号表。

既然有了符号表合并,显然[符号决议]需要考虑了(可见性下推链),这便是全局符号表中的符号优先级问题。

全局符号介入(Global Symbol Interpose):一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象。这是因为Linux下的ld.so是这样处理全局符号表合并的:当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。共享对象.so的装载顺序为广度优先。

由于存在这种重名符号被直接忽略的问题,当程序使用大量共享对象时应该非常小心符号的重名问题,如果两个符号重名又执行不同的功能,那么程序运行时可能会将所有该符号名的引用解析到第一个被加入全局符号表的使用该符号名的符号,从而导致程序莫名其妙的错误。

这样需要介绍到其实模块内部的全局函数的引用要和前面说过的全局变量一样不能采用内部相对地址引用,全部采用.got.plt的GOT跳转。这是因为如果要允许全局符号的覆盖,那么假设模块A内部存在全局bar()函数,模块B也存在一个功能不一样的bar()函数,并且先加载了模块B,这时全局符号表中bar()函数符号唯一起效的便是模块B中B::bar(),而若模块A内存在对bar()函数的引用,这时如果采用相对地址引用则要么是指引到A::bar(),这并不符合我们的设计预期,因为我们希望在整个程序中只有B::bar()起效,要么将模块A中对全局函数bar()的引用改成模块B中B::bar(),而这只能采用GOT表跳转实现。

因为很多模块都是默认函数是全局的外部可见的,这样便会导致内部函数引用时存在一步不必要的GOT表跳转,如果要加速函数调用效率,那么这便是关键字static的作用,static显示该文件内部的bar()函数为文件内部可见且不被外部覆盖,让调用逻辑更为清晰具体化,这时便可以采用近址调用的方式来实现函数调用,加快函数调用速度。

3.重定位和初始化

所有所需的共享对象.so一次性被装进进程空间后,则开始一次性的重定位和初始化。链接器在完成共享对象装载后,开始重新遍历可执行文件和每个共享对象的重定位表,开始对应的重定位工作。参考此前GOT/PLT重定位操作,并且这时已经有了所有模块的全局符号表合集,故而修正过程显然不需要再次遍历各个模块以寻找各目标符号的地址了。

重定位完成后,这是如果某个共享对象存在.init初始化段,则动态链接器会依次执行各共享对象的.init段代码,比如C++全局共享对象的初始化,相应的,在主程序结束后,还需要执行.finit段代码完成自定义的清场动作。具体的过程,会额外通过一篇文章介绍。

而主程序文件的.init段和.finit段并非由动态链接器完成,而是有运行库来实现封装。当所有的初始化工作完成后,开始将控制权转移到程序入口(运行库的初始入口函数)。

几个非常有意思的问题
Q1: 动态链接器本身是动态链接的还是静态链接的?
A: 静态链接的,因为动态链接器得自举,所以如果动态链接器也是动态链接的,那显然是各死循环。

$ldd  /lib/ld-linux.so.2
   statically linked

Q2:动态链接器本身得是PIC的吗?
A: 是动态链接可以是PIC的也可以不是,但使用PIC往往更简单一些,一方面是PIC可以使得代码段共享,另一方面则是ld.so自举过程比较复杂,如果代码段不是PIC的,则不仅需要对.data段还需要对.code段进行重定位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章