动态链接具体解释

                                                                                动态链接

 动态链接的诞生:

  动态链接产生最基本的原因就是静态链接空间浪费过于巨大,更重要的是现阶段各种软件都是模块化开发,不同模块都是由不同厂商开发的,一旦一个模块发生改变,整个软件就须要又一次编译(静态链接的情况下)。

 

动态链接主要思想:

     把链接这个过程推迟到了执行时再执行,这就是动态链接(Dynamic Linking)的基本思想。

 

动态链接的优点:

     1.动态链接将共享对象放置在内存中,不只节省内存,它还能够降低物理页面的换进换出,也能够提高CPU缓存的命中率,由于不同进程间的数据与指令都集中在同一个共享模块上。

     2.当一个软件模块发生改变的时,仅仅须要覆盖须要更新的文件。等程序下一次执行时自己主动链接更新那么,就算是跟新完毕了。

     3.添加程序的可扩展性和兼容性,它能够在执行时动态的载入各种程序模块。就是后来的插件(plug-in).

 

动态链接的基本实现:

     动态链接的基本思想就是把程序依照模块拆分成各个相对独立部分,在程序执行时才将他们链接在一起形成一个完毕的程序,而不是像静态链接把全部的模块都链接成一个单独的可执行文件。再Linux下ELF动态链接被称为动态共享库(DSO)。动态共享文件的制作參照之前的一片博文,后边将会对这个制作參数——FPIC 进行具体的介绍。

 

动态链接的数据结构:

       使用readelf 工具能够具体的查看动态链接文件(.so)文件,发现动态链接模块的装载地址是从0x0000000000開始的。可是这个地址是无效的地址。所以共享对象的终于装载地址在编译时是不确定的,而是在装载时依据当前的地址空间的空暇情况。动态分配一块足够大小的虚拟地址空间给对应的共享对象。

 

地址无关代码(fPIC)

       首先看看固定装载地址的困扰,假设都是装载地址都是固定好的,那么它的地址就是在编译时候确定的,那么当这个程序再须要添加模块或者程序段的时候,地址就会发生冲突,过去有人提出了改进这样的问题的方法,然而这并非一种好方法。叫做静态共享库,这样的方法将分配的指空间的任务交给了操作系统,操作系统在适当的时候给须要使用的模块预留地址空间。这样导致了非常多其他的问题产生,这样就须要一直保持全局变量的地址不变。升级了软件后全局变量的地址依旧不能变化。假设添加代码。变量等东西,会导致地址不够用。

简而言之,共享对象在编译时不能如果自己在进程虚拟地址空间中的为位置。

         重定位是一个解决这个问题的好方法,以重定位为基础开发了地址无关代码的技术,就是编译生成共享对象的那个參数。-fPIC 。

      首先我们分析下模块中各种类型的地址引用方式。

我们把共享对象模块中的地址引用依照是否须要跨模块分成两类:模块内部引用。模块外部引用。依照不同的引用方式又能够分为指令引用和数据訪问。

1.第一种是模块内部的函数调用。调转等。

2.另外一种是模块内部的数据訪问,比方模块内部的全局变量。静态变量等。

3.第三种是模块外部的函数调用,调转等。

4.第四种是模块外部的数据訪问,比方其他模块中定义的全局变量。

 

以下具体描写叙述一下这四种訪问方式:

第一种类型:

       第一种类型是最简单的就是模块内部的调用或调转,由于被调函数与调用函数都在函数内部,这在编译链接时他们的相对位置时确定的。这是相对地址调用,或者是基于寄存器的相对调用,所以这样的调用是不须要重定位的。

Call 指令地址:对于32位来说。第一个字节是指令的地址码,后边的四个字节是目的地址相对于当前指令的下一条指令的偏移。

另外一种类型:

       另外一种类型是数据模块内部的数据訪问。非常明显。指令中不能直接包括数据的绝对地址,那么唯一的办法就是相对寻址。可是怎样获得当前指令的地址是一个问题。由于现代操作系统,数据的相对寻址往往没有相对于当前指令的寻址方式,所以ELF 採用了一种巧妙的方法来获取当前指令地址(PC)。在汇编层面会调用一个_i686.get_pc_thunk.cx  的函数,作用是把返回地址的值放在ecx寄存器就是把call 的下一条指令地址放到ecx寄存器中。使用这个这样的方法能够确定模块内部的变量以及函数的入口。

 

第三种类型:

       模块间的数据訪问要更加复杂一些。首先还是须要地址无关的属性,毕竟是在多个模块中调用函数,变量。所以最好仅仅让它拥有一个父本。可是多个部分都能引用的上,所以ELF 在数据段里面建立一个指向这些变量的指针数组(*),也被称之为全局偏移表(GlobalOffset Table, GOT),当代码须要引用全局变量的时候通过GOT中对应的项间接引用。

当程序须要訪问变量的时候,首先通过另外一种方式得到PC的值,然后依据偏移量(偏移量在编译链接时就是已知的)找到GOT 段,在GOT中偏移就能够得到变量的地址。每个变量相应一个一个地址(4字节)至于相应的顺序是由编译器决定的。

 

第四种类型:

       模块间的函数调转与调用,和第三种类型相似,GOT段相同保存着各个函数的地址,相同运用另外一种方法首先找到PC 地址,然后加上一个偏移得到函数地址在GOT中的偏移,然后得到一个间接的调用。

 另:

       -fpic与 -fPIC ,这两个參数都是gcc 产生地址无关代码。唯一的差别是“-fPIC”产生的代码要大。而“-fpic”产生的代码相对较小,并且较快。可是fPIC在某些硬件平台上会有一些限制,比方全局符号的数量或者代码长度等而是“-fPIC”则没有这种限制,所以为了方便都使用“-fPIC”參数来产生地址无关代码。

Ps: 区分一个共享对象,是否为PIC:

       使用工具readelf 能够辨识。

    Readelf -d   foo.so | grep TEXTREL

     假设有输出那就不是PIC 的,否则就是PIC 的。PIC 的DSO 是不会包括不论什么代码段重定位表的。TEXTREL 表示代码段重定位表地址。

地址无关代码技术还能够使用在可运行文件上。一个以地址无关可运行方式产生參数位fpie.

延迟绑定(PLT):

 动态链接尽管节省了内存,可是牺牲了一些性能,所以为了改进性能就使用了新的技术延迟绑定技术(PLT),基本思想是:当函数第一次被使用时才进行绑定.

基本原理:当我们调用外部函数时。通常是通过訪问GOT段查找相应的项,进行调转。PLT(ProcedureLinkage Table)为了实现延时绑定,在中间又加入了一层调转。

调用函数并不通过GOT调转,而是通过一个叫PLT的项进行调转。每一个外部函数在PLT中都有一个相应的项称之为test@plt,如果由一个外部函数叫test( );那么在就会有这几行代码:

test@plt:

jmp*(test@plt)

push n

push moduleID

jump dl_runtime_resolve

 

 

test@plt:表示GOT中保存的test中的项。

这个项的第一条指令jmp指令是一条通过GOT之间调转的指令。

假设连接器在初始化的时候已经初始化了这个项,那么目标函数的地址已经填入该项,这就是我们期望的,直接调用就好还省的我们去GOT中找了,可是为了实现延时绑定,连接器并没有将函数地址放进去。而是将第二条指令push n 的地址增加到第一条指令中。这个步骤不须要查找不论什么一个符号表,代价非常低。也非常值的。

 

第二条指令将一个数字N(test 这个符号在rel.plt中的下标)压入栈中。

第三条指令将模块ID 压入堆栈。

第四条指令:调转到dl_runtime_resolve

这就是实现lookup(module,fun)函数的调用。

 

先将决议符号下标压入栈中,再将模块ID压入堆栈。然后调用连接器的dl_runtime_resolve()函数完毕符号解析和重定位工作。

这个函数在一系列的操作之后将test()函数的真正地址填入test@GOT,其中。此时GOT中才有了函数真正的函数,解也就解答了当时的一个问题。既然连接器能够把函数的地址填入GOT,为什么还有延时绑定。事实上仅仅有经历这些过程后函数地址才干被绑定。这就是PLT方式的基本实现,实际延时绑定的工作要比这样复杂的多。

 

GOT段的一些 信息:

  ELF 将GOT 段分成了两个部分,当中一个叫  . got  段,另一个为.got.plt .当中got 段用来保存全局变量引用的地址,got.plt 用来保存函数引用的地址。对于外部函数的引用所有被分离出来放到了got.plt 段表中。另外另一个特殊的地方。是它的前三项是有特殊意义的:

第一项保存的是.dynamic 的地址,这个段描写叙述了本模块动态链接相关信息。

第二项保存的是本模块的ID。

第三项保存的是_dl_runtime_reslove()函数的地址。

 

当中二三项是由动态链接器在装载共享模块的时候将负责将他们初始化。

 

动态链接相关结构:

       动态链接器:在interp 段保存了一些字符串。这些字符串就是动态链接器的地址。

当中动态链接结构中还包括dynamic段,动态符号表,动态链接重定位表,辅助信息组等信息,(初始化堆栈时初始化的一些信息,包括程序头。大小,类型,入口地址等)。

 

动态连接的步骤和实现:

       基本分成三步:先是启动动态链接器本身。

然后装载全部须要的共享对象,最后是重定为位和初始化。

首先是启动动态连接器本身,这就是动态连接器自举:

       动态链接器本身就是一个共享对象,它自己须要启动自己,别人启动靠他。可是他启动靠谁呢?,答案是仅仅能靠他自己,动态链接器不能依赖不论什么其他共享对象,其次它本身的全局变量,局部变量的重定位工作都由他自己完毕,完毕这些要求的代码很精细与静止。这样的代码往往被为自举。

动态连接器的入口地址就是字句代码的入口,当操作系统将控制权给动态连接器后,自举代码開始运行。首先訪问自己的GOT段。然后找到dynamic段, 找到自身的符号表,重定位表等从而得到动态链接器本身的重定为入口,先将他们重定位。从这一步開始时用自己的全局变量和静态变量。实际上在动态连接器在自举的过程中,除了不能够使用全局变量和静态变量之外甚至不能调用函数,时用PIC 模式编译的共享对象,对于模块内部函数调用和外部函数调用的方式是一样的。时用GOT/PLT方式,所以在GOT/PLT没有被重定位之前,自举代码不能够使用不论什么全局变量。

装载共享对象:

       完毕主要的自举以后,动态连接器将可运行文件和链接器本身的符号都合并到一个符号表其中。我们能够称之为全局变量。假设将这些装载的过程看作是一个图的遍历过程,使用的算法一般都是广度优先。

         当一个符号表须要被增加全局符号表时,假设同样的符号已经存在,则后增加的符号被忽略。

重定位和初始化:

       首先进行GOT/PLT重点定位完毕后。假设某个段有init 段,那么动态链接器就会运行init段中的代码,用以实现共享对象的初始化过程。比方C++的全局变量和静态对象,就须要通过init 来初始化,对应的可能还有 finit 段,当进程运行完毕后就会运行实现C++全局对象析构之类的操作。

 

Q&A:

动态链接器本身是静态还是动态?

事实上动态链接器本身是静态的。

  

动态链接器本身必须是IPC的吗?

是否是IPC 的对于动态链接器并不影响。可是假设是IPC 的就会更加的简单,实际上动态链接器就是PIC 的。

动态链接器能够被当成可运行文件,那么它的装载地址是多少?

动态链接器和普通的文件都是一样的。载入地址都是0x0000000000,这个地址是一个无效的地址,作为一个共享库,内核在装载后会为其选择一个合适的装载地址。

执行时载入:

 

事实上另一种更加灵活的载入方式。叫做显示执行时链接,有时候也叫做执行时载入,以共同拥有四个函数。

实际上是以库的形式调用的。基本是先打开库然后依据须要载入的函数符号载入对应的函数。所以一共同拥有这四个函数。

Dlopen( ) :用来打开一个动态库。并将其载入到进程的在地址空间

dlsym( ):这是载入的核心,我们能够通过这个函数找到所须要的符号

dlerror( ):错误处理,都能够用来推断上一次调用是否成功。

dlclose( ):将一个已经载入的模块卸载。系统会维持一个载入引用技术器。每次使用载入模块时。对应计数器加一,当使用这个函数卸载一个模块时数字就减一。

原文地址:https://www.cnblogs.com/zhchoutai/p/6993556.html