设为首页 加入收藏

TOP

Linux动态链接过程延迟绑定的实现
2019-04-02 22:08:49 】 浏览:123
Tags:Linux 动态 链接 过程 延迟 绑定 实现

动态链接的确有很多优势,比静态链接要灵活得多,但它是以牺牲一部分性能为代价的。据统计ELF程序在静态链接下要比动态库稍微快点,大约为1%~5%,当然这取决于程序本身的特性及运行环境等。我们知道动态链接比静态链接慢的主要原因是动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此一来,程序的运行速度必定会减慢。另外一个减慢运行速度的原因是动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器都要进行一次链接工作,正如我们上面提到的,动态链接器会寻找并装载所需要的共享对象,然后进行符号査找地址重定位等工作,这些工作势必减慢程序的启动速度。这是影响动态链接性能的两个主要问题,我们将在这一节介绍优化动态链接性能的一些方法。


在动态链接下,程序模块之间包含了大量的函数引用(全局变量往往比较少,因为大量的全局变量会导致模块之间耦合度变大),所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位,这也是我们上面提到的减慢动态链接性能的第二个原因。不过可以想象,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费。所以ELF采用了一种叫做延迟绑定(Lazy Binding)的做法,基本的思想就是当函数第一次被用到时才进行绑定(符号査找、重定位等),如果没有用到则不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加快程序的启动速度,特别有利于一些有大量函数引用和大量模块的程序。


ELF使用PLT( Procedure Linkage Table)的方法来实现,这种方法使用了一些很精巧的指令序列来完成。在开始详细介绍PLT之前,我们先从动态链接器的角度设想一下:假设 liba.so需要调用ibc.so中的bar(函数,那么当 liba. so中第一次调用bar()时,这时候就需要调用动态链接器中的某个函数来完成地址绑定工作,我们假设这个函数叫做 lookup(),那么 lookup)需要知道哪些必要的信息才能完成这个函数地址绑定工作呢?我想答案很明显, lookup至少需要知道这个地址绑定发生在哪个模块,哪个函数?那么我们可以假设lookup的原型为 lookup( module, function),这两个参数的值在我们这个例子中分别为 liba.so和bar()。在Glbc中,我们这里的 lookup函数真正的名字叫 _dl_ runtime_resolve()。


当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转。每个外部函数在PLT中都有一个相应的项,比如bar()函数在PLT中的项的地址我们称之为bar@plt。让我们来看看bar@plt的实现



bar@plt的第一条指令是一条通过GOT间接跳转的指令。bar@GOT表示GOT中保存bar()这个函数相应的项。如果链接器在初始化阶段已经初始化该项,并且将bar()的地址填入该项,那么这个跳转指令的结果就是我们所期望的,跳转到bar(0,实现函数正确调用但是为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入到该项,而是将上面代码中第二条指令“ push n”的地址填入到bar@GOT中,这个步骤不需要查找任何符号,所以代价很低。很明显,第条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将一个数字n压入堆栈中,这个数字是bar这个符号引用在重定位表“ rel. plt”中的下标接着又是一条push指令将模块的D压入到堆栈,然后跳转到dl_ runtime resolve这实际上就是在实现我们前亩提到的 lookup( module, function)这个函数的调用:先将所需要决议符号的下标压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的d_ runtime_ resolve()函数来完成符号解析和重定位作。 dl_runtime_resolve在进行一系列工作以后将bar(的真正地址填入到bar@GOT中


一旦bar()这个函数被解析完成,当我们再次调用bar@plt时,第一条jmp指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈里面保存的EIP直接返回调用者,而不会再继续执行bar()plt中第二条指令的开始的那段代码,那段代码指挥在符号未被解析的时候执行一次;


上面描述的是PLT的基本原理,PLT的真正实现要比它的结构复杂一些,ELF将GOT拆分成两个表".got"和"".got.plt"。其中"".got"用来保存全局变量的引用地址。".got.plt"用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了 ".got.plt"中。另外 ".got.plt"还有一个特殊的地方就是它的前三项是有特殊意义的,分别含义如下:


其中第二项和第三项由动态链接器在转载共享模块的时候负责将它们初始化。".got.plt"的其余项分别对应每一个外部函数的引用。PLT的结构也与我们示例中的PLT稍有不同,为了减少代码的重复,ELF把上面的例子中的最后两条指令放在PLT中的第一项,并且规定每一项是16个字节,刚好用来存放3条指令,实际上的PLT的基本结构如图所示:



实际上的PLT结构如下:


PLT在ELF文件中以独立代码的段存放,段名通常称为 ".plt",因为它本身就是一些地址无关的代码,所以可以跟代码段一起合并成同一个可读可执行的""Segment"被装载到内存中


】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇动态链接的相关结构 下一篇关于local_irq_disable和disable_..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目