动态链接的整个过程

来源:互联网 发布:install ubuntu reboot 编辑:程序博客网 时间:2024/05/22 08:02

 

1.静态链接的缺点

(1)同一个模块被多个模块链接时,那么这个模块在磁盘和内存中都有多个副本,导致很大一部分空间被浪费了。

(2)当程序的任意一个模块发生更新时,整个程序都要重新链接、发布给用户。

2.动态链接的基本思想

动态就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

3.链接文件

Linux 中,ELF的动态链接文件被称为动态共享对象(DSO,dynamic shared object),以.so为扩展名的文件。

在WINDOWS中,EP的动态链接文件被称为动态链接库(DLL,dynamic link library),以 .dll 为扩展名的文件。

4.动态链接程序运行时地址空间的分布

在动态链接程序运行时,除了可执行文件本身,将还有动态链接文件与动态链接器将被映射到进程的地址空间中,动态链接器被当做普通的共享对象来进行映射,在系统运行可执行文件之前,会将控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给可执行文件。

共享对象的装载方式:

1.固定装入方式

由程序员手动指定或者由操作系统给共享对象分配固定的装载地址。

存在的问题:

(1)地址冲突

假设A程序员开发模块a时,将其的地址固定在0x1000到0x2000,B程序员开发模块b时,也将其地址固定在了相同的位置,则现在程序员C开发的程序需要同时用到 a 与 c 模块,那就无法解决这个问题了

(2)静态共享库的升级困难

在链接时,对所有绝对地址的引用不做重定位,把这一步推迟到装载的时候进行;一旦模块装载地址确定,即目标地址确定,则系统对程序中所有的绝对地址的引用进行重定位。

2.装载时重定位(LTR,load time relocation)

在链接时,对所有绝对地址的引用不做重定位,把这一步推迟到装载的时候进行;一旦模块装载地址确定,即目标地址确定,则系统对程序中所有的绝对地址的引用进行重定位。解决了冲突问题如果一个程序在编译时,假设目标地址为0x1000,但在装载的时候操作系统发现0x1000已经被占用,且从0x4000开始有一块足够大的空间可以容纳该程序,则就将该程序装载到0x4000,则程序中指令和数据中的所有绝对引用都只要再加上0x3000的偏移量就可以了。

缺点:

指令部分没法共享,因为根据装入时的重定位,共享对象的装入地址是不确定的。根据指令修正的方式:绝对寻址:S(符号的实际地址)+A(保存在修正位置的值),相对寻址:S(符号的实际地址)+A(保存在修正位置的值)-P(被修正的位置)。可以看出对不同的程序共享对象的指令修正后的指令不相同。这样就没法实现共享。

5.PIC(地址无关代码)

动态链接库希望所有进程共享指令段而各自拥有数据段的私有副本,为了实现这个目标,就要采用与地址无关代码(PIC: Position Independent code)的技术。该实现的基本思想是:把指令中需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分则在每个进程拥有一个副本。
与地址无关的代码,也就是需要考虑代码中会对地址进行引用的情况,共享对象(GCC中的动态链接文件)中地址引用可以分为以下几种情况:
a) 模块内函数调用、跳转等
b) 模块内数据的访问,如模块内定义的静态变量,全局变量等
c) 模块外部的函数调用、跳转等
d) 模块外部的数据的访问,比如别的模块定义的全局变量

wps606A.tmp

(1)模块(动态链接文件)内部的函数的调用

         由于此时调用者与被调者都是位于同一个模块,所以调用者与被调者之间的相对位置是固定的,因此,对被调者的调用就可以使用相对地址来替代绝对地址,对于这种指令就是不需要重定位的。

eg: 对上面代码中的 foo() 与 bar() 函数,因为bar() 函数相对 foo()函数中的调用语句之间的距离是固定不变的,因此该语句将是与地址无关的。

(2)模块内部的数据调用

         与上面分析同理,由于数据定义与引用指令是位于同一个模块的,因此它们之间的相对位置是固定的。但是此时有一些区别,现代体系结构中,数据的相对寻址没有基于当前指令的寻址方式,因此 ELF 采用了一个巧妙的方法来获取当前的PC(程序计数器)的值,再在该基础上添加一个偏移,即可访问到变量。

获取当前PC的方法:调用 “__i686.get_pc_thunk.cx”函数,该函数的作用是将返回地址的值放入ecx寄存器中,即把 call 的下一句指令的地址放入 ecx 寄存器中。(以上面的例子,操作是将 a = 1 该句指令的地址放入寄存器)

实现将返回地址放入寄存器的方法:处理器执行 call 指令后,下一条指令的地址会被压到栈顶,而 esp 寄存器指向栈顶,那么当“__i686.get_pc_thunk.cx”执行 "mov (%esp) ecx" 的时候,返回地址就放入了寄存器。

(3)模块外部的函数的调用

         此时对外部符号的引用显然是与地址有关的,按照先前说的基本思想,此时需要将与地址相关的部分放到数据段里。ELF 的做法是在数据段中建立一个指向这些函数的指针数组,也即是全局偏移表(GOT,Global Offset Tabel),当代码需要引用这些外部函数时,则可以通过GOT 中的相对应的项间接引用。

动态链接器在装载模块的时候会查找每个函数所在地址,并填充GOT中的各个表项,以保证每个指针均指向正确的地址。同时由于GOT本身是放在数据段的,因此它可以在模块装载的时候被修改,并且每个进程都可有自己的副本。

(4)模块外部的数据的调用

该方法与模块外部的函数访问方法相同,同样引入 GOT ,只是此时GOT 的表项中存储的是变量的地址。

5.Plt(延迟绑定)

装载时的重定位存在的问题:

(1)我们知道动态链接比静态链接慢的主要原因是动态链接下对于全局和静态

的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此一来,程序的运行速度必定会减慢。

(2)另外一个减慢运行速度的原因是动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器都要进行一次链接工作。

当在程序运行过程中需要调用动态链接器来为某一个第一次调用的外部函数进行地址绑定时,需要提供给动态链接器的内容有:发生地址绑定需求的地方(文件名)以及需要绑定的函数名,也即是说,假设动态链接器使用某一个函数来进行地址绑定工作,那它的函数原型应该为: lookup(module,function)。事实上,在Glibc 中,该 lookup函数的真实名字是:_dl_runtime_resolve()

Plt的简单实现

原来的做法:调用某一个外部函数时,通过GOT中的相应项进行间接跳转PLT 的加入Plt之后:调用函数时,通过一个 PLT项的结构来进行跳转,每一个外部函数在 PLT 中都有一个相应的项

Plt的基本实现:

bar@plt:

jmp *(bar@GOT)          //如果是第一次链接,该语句的效果只是跳转到下一句指令。否则,将会跳转到 bar()函数对应的位置

push n //压栈 n,n 是 bar 这个符号在重定位表 .rel.plt 中的下标

push moduleID            // 压栈当前模块的模块ID,上述例子中的 liba.so

jump _dl_runtime_resolve()    //跳转到动态链接器中的地址绑定处理函数

函数和模块ID都有了可以使用Lookup()函数,求出真正的bar()地址。当我们再次调用bar@plt时,第一条jmp指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈里面保存的EIP直接返回到调用者,而不会再继续执行bar@plt中第二条指令开始的那段代码,那段代码只会在符号未被解析时执行一次。

在实际实现中

ELF将GOT拆为了两个表叫做“.got”,".got.plt"。其中 .got 用来保存全局变量的引用地址,.got.plt 用来保存函数引用的地址,也就是说,所有对于外部函数的引用被分离到了 .got.plt 表中,该表的前三项分别是:

1. .dynamic 段的地址

2. 本模块的 ID

3. _dl_runtime_resolve()的地址

7.与动态链接相关的段

1 .interp 段

动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由 ELF 文件中的 .interp 段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于 /lib/ld-linux.so.2。(通常是软链接)

2 .dynamic 段

该段中保存了动态链接器所需要的基本信息,是一个结构数组,可以看做动态链接下 ELF 文件的“文件头”。存储了动态链接会用到的各个表的位置等信息。

3 .dynsym 段

该段与 “.symtab”段类似,但只保存了与动态链接相关的符号,很多时候,ELF文件同时拥有 .symtab 与 .synsym段,其中 .symtab 将包含 .synsym 中的符号。该符号表中记录了动4 .dynstr 段

该段是 .dynsym 段的辅助段,.dynstr 与 .dynsym 的关系,类比与 .symtab 与 .strtab 的关系

5 .hash 段

在动态链接下,需要在程序运行时查找符号,为了加快符号查找过程,增加了辅助的符号哈希表,功能与 .dynstr 类似

态链接符号在动态符号字符串表中的偏移,与.symtab中记录对应。

6 .rel.dyn 段

对数据引用的修正,其所修正的位置位于 “.got”以及数据段(类似重定位段 "rel.data")

7 .rel.plt 段

对函数引用的修正,其所修正的位置位于 “.got.plt”

关于地址的修正,这两个段就相当于重定位表,记录了哪些函数需要进行重定位,以及如何进行重定位。got.plt记录的是符号的地址,在寻找这个地址的时候要重定位

8.动态链接器

         动态链接器是一个特殊的共享对象,它不仅是共享对象,还是可执行文件。linux的内核在执行execve()时不关心目标ELF文件是否可执行,它的工作是按照程序头表里的描述对文件进行装载然后将控制权转给ELF的入口地址。而共享对象其实也是ELF文件,也有跟可执行文件一样的ELF文件头,因此,动态链接器本身就可以作为可执行文件运行。(如果没有“.interp”就是动态链接器,否则是动态链接可执行文件)

(1)动态链接器本身是静态链接的,且不能依赖别的共享对象,即满足自举(对于动态链接器的第一个要求我们可以认为的控制,在编写动态链接器时不适用任何系统库,运行库;但对第二个要求,动态链接器必须在启动时有一段非常精巧的代码可以完成重定位工作同时有不能用到全局和静态变量。这种有限制的启动代码称为自举)。

(2)动态链接器本身可以是PIC的也可以不是,但若是PIC的会简单些。否则,动态链接器的代码段将无法共享,浪费内存。

(3)动态链接器可被当做可执行文件,且其装载地址是0x00000000,这是一个无效装载地址,作为一个共享库,内核在装载它的时候会为其选择合适的装载地址。

9.Linux的动态链接的实现

(1)动态链接器获取自身的符号表和重定位表,以及自身的入口地址

         动态链接器本身不可以依赖于其他任何共享对象;其次是动态链接器本身所需要的全局和静态变量的重定位工作由它本身完成。动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始执行。自举代码首先会找到它自己的GOT。而GOT的第一个入口保存的即是“.dynamic”段的偏移地址,由此找到了动态连接器本身的“.dynamic”段。通过“.dynamic”中的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位。从这一步开始,动态链接器代码中才可以开始使用自己的全局变量和静态变量。

(2)将可执行文件和链接器本身的符号表都合并到一个全局符号表中,然后链接器在“.dynamic”段中找到该可执行文件依赖的共享对象,链接器将这些共享对象的名字加入到一个装载集合中

         动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。然后链接器开始寻找可执行文件所依赖的共享对象,我们前面提到过“.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。然后链接器开始从集合里取一个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和“.dynamic”段,然后将它相应的代码段和数据段映射到进程空间中。如果这个ELF共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止,当然链接器可以有不同的装载顺序,如果我们把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法一般都是广度优先的。

当一个新的共享对象被装载进来的时候,它的符号表会被合并到全局符号表中,所以当所有的共享对象都被装载进来的时候,全局符号表里面将包含进程中所有的动态链接所需要的符号。

(3)当所有共享对象加载完毕之后,动态链接器将控制权交回给ELF头中指定的程序入口地址。

原创粉丝点击