Linux加载启动可执行程序的过程(二)解释器完成动态链接

接着上一篇博客。前面的工作都是在内核完成的,接下来会回到用户空间。

第一步,解释器(也可以叫动态链接器)首先检查可执行程序所依赖的共享库,并在需要的时候对其进行加载。

ELF 文件有一个特别的节区: .dynamic,它存放了和动态链接相关的很多信息,例如动态链接器通过它找到该文件使用的动态链接库。不过,该信息并未包含动态链接库的绝对路径,但解释器通过 LD_LIBRARY_PATH 参数可以找到(它类似 Shell 解释器中用于查找可执行文件的 PATH 环境变量,也是通过冒号分开指定了各个存放库函数的路径)该变量实际上也可以通过/etc/ld.so.conf 文件来指定,一行对应一个路径名。为了提高查找和加载动态链接库的效率,系统启动后会通过 ldconfig 工具创建一个库的缓存 /etc/ld.so.cache 。如果用户通过 /etc/ld.so.conf 加入了新的库搜索路径或者是把新库加到某个原有的库目录下,最好是执行一下 ldconfig 以便刷新缓存。

找到动态链接库后,就可以将其加载到内存中。

第二步,解释器对程序的外部引用进行重定位,并告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态链接还有一个延迟定位的特性,即只有在“真正”需要引用符号时才重定位,这对提高程序运行效率有极大帮助。(如果设置了 LD_BIND_NOW 环境变量,这个动作就会直接进行)

下面具体说明符号重定位的过程。

首先了解几个概念。符号,也就是可执行程序代码段中的变量名、函数名等。重定位是将符号引用与符号定义进行链接的过程,对符号的引用本质是对其在内存中具体地址的引用,所以本质上来说,符号重定位要解决的是当前编译单元如何访问「外部」符号这个问题。动态链接是在程序运行时对符号进行重定位,也叫运行时重定位(而静态链接则是在编译时进行,也叫链接时重定位)

现代操作系统中,二进制映像的代码段不允许被修改,而数据段能被修改。

编写如下代码

通过gcc编译成.o文件后,再通过objdump-d命令得到文件的汇编指令,如下所示

call指令的操作数是fc ff ff ff,翻译成16进制数是0xfffffffc,看成有符号是-4。这里应该存放printf函数的地址,但由于编译阶段无法知道printf函数的地址,所以预先放一个-4在这里。所以程序为了正确执行,需要在链接时对其地址进行修正。这里的原理对静态链接和动态链接来说都是一样的。

但对于动态链接来说,有两个不同的地方:

(1)因为不允许对可执行文件的代码段进行加载时符号重定位,因此如果可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在构建可执行文件的时候,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的内存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。

(2)ELF 文件对调用动态库中的函数采用了所谓的"延迟绑定"(lazy binding)策略, 只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因此我们不需要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连修改动态库和可执行程序中的相应代码都不需要进行了,当然延迟绑定的目的不是为了这个,具体先不细说。

可执行程序对符号的访问又分为模块内和模块间的访问,这里只介绍模块间的访问,也就是访问动态链接库中的符号。

通过gcc生成test可执行文件,然后同样用objdump-d得到可执行文件的汇编指令,如下所示

可以看到这里的call指令指向了80482e0地址处,也即是PLT。

PLT就是程序链接表(Procedure Link Table),属于代码段。用于把位置独立的函数调用重定向到绝对位置。每个动态链接的程序和共享库都有一个PLT,PLT表的每一项都是一小段代码,从对应的GOT表项中读取目标函数地址。程序对某个函数的第一次访问都被调整为对 PLT入口也就是PLT0的访问,也就是说所有的PLT首次执行时,最后都会跳转到第一个PLT中执行。PLT0是一段访问动态链接器的特殊代码,是动态链接做符号解析和重定位的公共入口。这样做的好处是不用每个PLT表都有重复的一份指令,可以减少PLT指令条数。

PLT表结构如下图所示

可以看到,PLT会先执行jmp指令跳转到某一个地址,而这个地址就对应的GOT表项。

GOT就是全局偏移表(Global Offset Table),属于数据段。为了能使得代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转,ELF 的做法是在动态库的数据段中加一个表项,也就是GOT 。GOT表格中放的是数据全局符号的地址,该表项在动态库被加载后由动态加载器进行初始化,动态库内所有对数据全局符号的访问都到该表中来取出相应的地址,即可做到与具体地址了,而该表作为动态库的一部分,访问起来与访问模块内的数据是一样的。

GOT表结构如下图所示

GOT[0]对应本ELF动态段(.dynamic段)的装载地址,GOT[1]对应本ELF的link_map数据结构描述符地址,GOT[2]:对应_dl_runtime_resolve动态链接器函数的地址。3个特殊项后面依次是每个动态库函数的GOT表项

上面讲到PLT通过jmp指令跳转到GOT表中去取函数的真实地址,而符号所对应的表项开始是没有这个地址的,而是存放了该PLT表项jmp指令的下一条指令地址,也就是push指令。回到了PLT表项对应的指令中继续执行,最后一条jmp指令跳转到了PLT0中执行。

PLT0对应的指令执行了下列过程:首先pushl把 804a004(GOT[1])这块内存里的qword入栈,这个qword是link_map的地址,根据这个地址可以找到动态库的符号表。然后jmp跳转到GOT表中的第三项,找到动态链接器的_dl_runtime_resolve函数地址,开始执行该函数。回想前面讲到的内核中加载目标映像的过程,可执行文件在Linux内核通过exeve装载完成之后,不直接执行,而是先跳到动态链接器(ld-linux-XXX)执行。在ld-linux-XXX里将link_map地址、_dl_runtime_resolve地址写到GOT表项内。所以在此时,该GOT表项的不为空。(前面三个GOT表项都是这样被写入的)然后当程序加载其它动态库的时候,会把动态库的符号信息插入link_map


_dl_runtime_resolve函数得到动态链接库中函数的地址后(该过程以后再分析),写回到对应的GOT表项中。

这就是函数第一次被调用时执行的过程。以后每次被调用直接从GOT表中取到函数地址就可以了。

总的来说,动态重定位的过程可以由下图表示

部分内容和图片参考:https://blog.csdn.net/linyt/article/category/6267121

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