动态链接性能优化及相关结构*

动态链接确实有很多优势,比静态链接要灵活的多,但是它是以牺牲一部分性能为代价的。

 

性能优化:

  • 延迟绑定(PLT):当函数第一次调用时,才进行绑定(符号查找,重定位等)

当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中的相应的项进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又加了一层间接跳转。调用函数通过一个叫PLT项的结构来进行跳转。大概就是如果该函数的地址已经在GOT中,则直接跳转,如果没有,则先进行重定位。

 

ELF将GOT拆分成了两个表叫做“.got" 和“.got.plt”"。 其中“.got" 用来保存全局变最引用的地址,“.got.plt" 用来保存丽数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了“.got.plt" 中。另外“.got.plt" 还有一个特殊的地方是它的前三项是有特殊意义的,分别含义如下:


第一项保存的是“.dynamic”"段的地址,这个段描述了本模块动态链接相关的信息
第二项保存的是本模块的ID。
第三项保存的是_ dl_ _runtime. _resolve()的地址。

 

动态链接的相关结构

动态链接情况下,可执行文件的装载与静态链接情况基本一样。首先操作系统会读取可执行文件的头部,检查文件的合法性,然后从头部中的“ProgramHeader”中读取每个“Segment”的虚拟地址、文件地址和属性,并将它们]映射到进程虛拟空间的相应位置,这些步骤跟前面的静态链接情况下的装载基本无异。在静态链接情况下,操作系统接着就可以把控制权转交给可执行文件的入口地址,然后程序开始执行,一切看起米非常直观。但是在动态链接情况下,操作系统还不能在装载完可执行文件之后就把控制权交给可执行文件,因为我们知道可执行文件依赖于很多共享对象。这时候,可执行文件里对于很多外部符号的引用还处于无效地址的状态,即还没有跟相应的共享对象中的实际位置链接起来。

所以在映射完可执行文件之后,操作系统会先启动一个动态链接器( Dynamic Linker)。在Linux下,动态链接器ld.so 实际上是-一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间中。操作系统在加载完动态链接器之后,就将控制权交给动态链接器的入口地址(与可执行文件-样,共享对象也有入口地址)。当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。

 

在动态链接的ELF中有一个专门的段:.interp 段保存了一个字符串,就是可执行文件所需要的动态链接器的路径。

.dynamic 段:可以看成动态链接下ELF文件的“文件头”。该段肯定也是ELF文件头所指示的段表中的一个。

动态符号表:保存了与动态链接相关的符号(很多时候动态链接的模块同时拥有动态符号表和符号表)

符号哈希表(类似字符串表)

动态链接重定位表:动态链接下,无论是可执行文件或共享对象,一旦它依赖于其他共享对象,它的代码和数据中就会有导入符号的引用,需要进行重定位。PIC模式的共享对象只能保证代码段不需要重定位,数据段仍需要重定位,重定位不可避免。

当动态链接器需要进行重定位时,它先查找“printf"的地址,“printf"位于libc-2.6.1.so,假设链接器在全局符号表里面找到“printf"的地址为0x08801234,那么链接器就会将这个地址填入到“.got.plt"中的偏移为0x000015d8 的位置中去,从而实现了地址的重定位,即实现了动态链接最关键的一个步骤。

如果某个ELF文件是以PIC模式编译的,则外部引用会出现在.rel.plt 中, 如果采用非PIC模式, 则外部引用会出现在.rel.dyn 中。

 

动态链接时进程堆栈初始化信息

站在动态链接器的角度看,当操作系统把控制权交给它的时候,它将开始做链接工作,那么至少它需要知道关于可执行文件和本进程的一些信息,比如可执行文件有几个段(“Segment").每个段的属性、程序的入口地址(因为动态链接器到时候需要把控制权交给可执行文件)等。这些信息往往由操作系统传递给动态链接器,保存在进程的堆栈里面。进程初始化的时候,堆栈里面保存了关于进程执行环境和命令行参数等信息。事实上,堆栈里面还保存了动态链接器所需要的一些辅助信息数组(Auxiliary Vector)。

 

posted @ 2022-12-28 10:01  stu--wy  阅读(138)  评论(0)    收藏  举报