接着上一篇博客。后面的工作都是在内核完成的,接出来会回到用户空间。
第一步,原语(也可以叫动态链接器)首先检测可执行程序所依赖的共享库,并在须要的时侯对其进行加载。
ELF文件有一个非常的节区:.dynamiclinux shell,它储存了和动态链接相关的好多信息,比如动态链接器通过它找到该文件使用的动态链接库。不过,该信息并未包含动态链接库的绝对路径,但类库通过LD_LIBRARY_PATH参数可以找到(它类似Shell类库中用于查找可执行文件的PATH环境变量,也是通过逗号分开指定了各个储存库函数的路径)该变量实际上也可以通过/etc/ld.so.conf文件来指定,一行对应一个路径名。为了提升查找和加载动态链接库的效率linux 解释器文件,系统启动后会通过ldconfig工具创建一个库的缓存/etc/ld.so.cache。假如用户通过/etc/ld.so.conf加入了新的库搜索路径或则是把新库加到某个原有的库目录下,最好是执行一下ldconfig便于刷新缓存。
找到动态链接库后,就可以将其加载到显存中。
第二步,协程对程序的外部引用进行重定位,并告诉程序其引用的外部变量/函数的地址,此地址坐落共享库被加载在显存的区间内。动态链接还有一个延后定位的特点,即只有在“真正”需要引用符号时才重定位,这对增强程序运行效率有极大帮助。(假如设置了LD_BIND_NOW环境变量,这个动作都会直接进行)
下边具体说明符号重定位的过程。
首先了解几个概念。符号,也就是可执行程序代码段中的变量名、函数名等。重定位是将符号引用与符号定义进行链接的过程,对符号的引用本质是对其在显存中具体地址的引用,所以本质上来说,符号重定位要解决的是当前编译单元怎么访问「外部」符号这个问题。动态链接是在程序运行时对符号进行重定位,也叫运行时重定位(而静态链接则是在编译时进行,也叫链接时重定位)
现代操作系统中,二补码映像的代码段不容许被更改,而数据段能被更改。
编撰如下代码
通过gcc编译成.o文件后,再通过objdump-d命令得到文件的汇编指令,如下所示
call指令的操作数是fcffffff,翻译成16补码数是0xfffffffc,看成有符号是-4。这儿应当储存printf函数的地址,但因为编译阶段未能晓得printf函数的地址,所以预先放一个-4在这儿。所以程序为了正确执行,须要在链接时对其地址进行修正。这儿的原理对静态链接和动态链接来说都是一样的。
但对于动态链接来说linux 解释器文件,有两个不同的地方:
(1)由于不容许对可执行文件的代码段进行加载时符号重定位,因而假如可执行文件引用了动态库中的数据符号,则在该可执行文件内对符号的重定位必须在链接阶段完成,为做到这一点,链接器在建立可执行文件的时侯,会在当前可执行文件的数据段里分配出相应的空间来作为该符号真正的显存地址,等到运行时加载动态库后,再在动态库中对该符号的引用进行重定位:把对该符号的引用指向可执行文件数据段里相应的区域。
(2)ELF文件对调用动态库中的函数采用了所谓的"延后绑定"(lazybinding)策略,只有当该函数在其第一次被调用发生时才最终被确认其真正的地址,因而我们不须要在调用动态库函数的地方直接填上假的地址,而是使用了一些跳转地址作为替换,这样一来连更改动态库和可执行程序中的相应代码都不须要进行了,其实延后绑定的目的不是为了这个,具体先不细说。
可执行程序对符号的访问又分为模块内和模块间的访问,这儿只介绍模块间的访问,也就是访问动态链接库中的符号。
通过gcc生成test可执行文件,之后同样用objdump-d得到可执行文件的汇编指令,如下所示
可以看见这儿的call指令指向了80482e0地址处,也即是PLT。
PLT就是程序链接表(ProcedureLinkTable),属于代码段。用于把位置独立的函数调用重定向到绝对位置。每位动态链接的程序和共享库都有一个PLT,PLT表的每一项都是一小段代码,从对应的GOT表项中读取目标函数地址。程序对某个函数的第一次访问都被调整为对PLT入口也就是PLT0的访问,也就是说所有的PLT首次执行时,最后就会跳转到第一个PLT中执行。PLT0是一段访问动态链接器的特殊代码,是动态链接做符号解析和重定位的公共入口。这样做的用处是不用每位PLT表都有重复的一份指令,可以降低PLT指令条数。
PLT表结构如右图所示
可以看见,PLT会先执行jmp指令跳转到某一个地址,而这个地址就对应的GOT表项。
GOT就是全局偏斜表(GlobalOffsetTable),属于数据段。为了能促使代码段里对数据及函数的引用与具体地址无关,只能再作一层跳转linux培训机构,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表中取到函数地址就可以了。
总的来说,动态重定位的过程可以由右图表示
部份内容和图片参考: