DSO(dynamic shared object)动态共享对象的原理


摘要:可执行ELF文件的生成,如果从源文件c开始算起,需要经过编译、链接、装载几个步骤。在链接和装载这两个步骤中,有两种不同的实现方式,一种是静态链接,这样加载之前,已经形成可执行目标文件;另外一种是动态链接,这种模式下,链接阶段只是链接一部分目标文件,动态链接库(DSO)在装载的时刻才会映射到进程地址空间。本文主要讲解DSO的原理。


1.装载过程涉及的问题

当程序被装载时,系统的动态链接器会将程序所需的所有动态链接库(例如最基本的libc.so)装载到进程的地址空间,且将程序中所有为决议的符号绑定到相应的动态链接库中,并进行重定位工作(术语叫装载时重定位-load time relocation,在windows中,又叫基址重置-rebasing,区别于静态链接的链接时重定位-link time relocation)。也即,动态链接是把可执行elf的形成过程从本来的程序链接前推迟到装载时。共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。        

        装载时重定位的问题:so文件被load并映射至虚拟空间后,指令部分通常是多个进程间共享的,通常的装载时重定位是通过修改指令实现的(主要是根据情况修改指令中涉及到的地址),所以无法做到同一份指令被多个进程共享(因为指令被重定位后对每个进程来讲是不同的)。这样一来,就失去了动态链接节省memory的一大优势。

         为解决此问题,引入了地址无关代码(PIC,Position-independent Code,详细概念见wikipedia)的技术,基本思路是把指令中那些需要被修改的部分分离出来,跟数据部分放到一起,这样,剩下的指令就可以保持不变,而数据部分在每个进程中拥有一个副本。ELF针对各种可能的访问类型(模块内部指令调用、模块内部数据访问、模块间指令调用、模块间数据访问),实现了对应地址引用方式,从而实现了PIC。

        对应到实际应用中,我们可以在编译时指定-fPIC参数让gcc产生地址无关码。


2.印象动态链接性能的因素


        a. 与静态链接相比,动态链接对全局和静态的数据访问都要进行GOT(Global Offset Table,实现PIC时引入的具体技术)定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后间接跳转,如此,程序的运行速度就会减慢

       b. 程序装载时,动态链接器要进行一次链接工作,即查找并装载所需的共享对象,然后进行符号查找、地址重定位等工作,这会减慢程序启动速度

       一方面,程序模块往往包含了大量的函数调用,从而导致动态链接器在模块间函数引用的符号查找及重定位方面耗费时间;另一方面很多函数并不会在程序运行初期就用到(尤其是有些异常处理函数),由此,EFL采用延迟绑定(lazy binding)来对动态链接做优化,其基本思想是当函数第一次被调用时才进行绑定(符号查找、重定位等),若没有被调用则不进行绑定。这个思路可以大大加快程序启动速度,对于有大量函数引用的程序启动时,尤为明显。具体到实现,EFL采用PLT(procedure linkage table)来实现,具体过程很是精妙复杂,本文只是抛砖引玉,不再详述,有兴趣的同学可以用PLT英文关键字google相关资料



3.疑问

1)是否能够用地址映射共享DSO内存

同一份指令应当是指的共享模块中的指令吧?如果是的话,那么各个进程无论将共享模块装载至进程虚拟地址空间的何处,只要在该进程装载时对共享模块中地址的引用执行重定位,即使装载的地址不一样,重定位之后也是各个进程的中的代码对共享模块引用地址的不同,而共享模块中的代码是不需要变化的?


分析:简而言之,DSO是不是可以不修改代码,而是通过将不同进程的虚拟地址映射到同一个区域的物理地址来实现,DSO的共享?


不要忘了,装载进去的都是指令,也就是一行行的“汇编代码”:

mov eax, [0x123456]

很显然,如果DSO引用了外部模块的地址,那么这个地址的变量值总是随着整个进程空间布局的不同而变化。那么,这个问题就不是能用linux内核的内存管理中虚实地址的映射能够解决的了。

你可能感兴趣的:(库,动态链接,DSO)