动态链接
动态链接
要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接( dynamic linking)的基本思想。
动态库的基本实现
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个个单独的可执行文件。那么我们能不能按照前面例子中所描述的那样,直接使用目标文件进行动态链接呢?这个问题的答案是:理论上是可行的,但实际上动态链接的实现方案与直接使用目标文件稍有差别。我们将在后面分析目标文件和动态链接文件的区别。
动态链接涉及运行时的链接及多个文件的装载,必需要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂,还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。目前主流的操作系统几乎都支持动态链接这种方式,在 linux系统中,elf动态链接文件被称为动态共享对象(dso, dynamic shared objects),简称共享对象,它们一般都是以“so”为扩展名的一些文件; 而在 windows系统中,动态链接文件被称为动态链接库( dynamical linking library),它们通常就是我们平时很常见的以“dll”为扩展名的文件。
从本质上讲,普通可执行程序和动态链接库中都包含指令和数据,这一点没有区别。在使用动态链接库的情况下,程序本身被分为了程序主要模块( progran1)和动态链接库( lib. so),但实际上它们都可以看作是整个程序的一个模块,所以当我们提到程序模块时可以指程序主模块也可以指动态链接库。
在 linux中,常用的c语言库的运行库glib,它的动态链接形式的版本保存在“/lib”目录下,文件名叫做“ libc.so”。整个系统只保留一份c语言库的动态链接文件“libc.so”,而所有的c语言编写的、动态链接的程序都可以在运行时使用它。当程序被装载的时候,系统的动态链接器会将程序所需要的所有动态链接库(最基本的就是libc.so)装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。
程序与libc.so之间真正的链接工作是由动态链接器完成的,而不是由我们前面看到过的静态链接器ld完成的。也就是说,动态链接是把链接这个过程从本来的程序装载前被推迟到了装载的时候。可能有人会问,这样的做法的确很灵活,但是程序每次被装载时都要进行重新进行链接,是不是很慢?的确,动态链接会导致程序在性能的一些损失,但是对动态链接的链接过程可以进行优化,比如我们后面要介绍的延迟绑定( lazy binding)等方法,可以使得动态链接的性能损失尽可能地减小。据估算,动态链接与静态链接相比,性能损失大约在5%以下。当然经过实践的证明,这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性,是相当值得的。
动态链接程序运行时地址空间分布
对于静态链接的可执行文件来说,整个进程只有一个文件要被映射,那就是可执行文件本身,我们在前面的章节己经介绍了静态链接下的进程虚拟地址空间的分布。但是对于动态链接来说,除了可执行文件本身之外,还有它所依赖的共享目标文件。那么这种情况下,进程的地址空间分布又会怎样呢?
我们还是以上面的 program1为例,但是当我们试图运行 program1并且查看它的进程空间分布时,程序一运行就结束了。所以我们得对程序做适当的修改,在libc中的 foobar.c 函数里面加入sleep函数
然后就可以查看进程的虚拟地址空间分布:
我们看到,整个进程虚拟地址空间中,多出了几个文件的映射。lib.so 与 program1 一样,它们都是被操作系统用同样的方法映射至进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同。 programi除了使用lb.so以外,它还用到了动态链接形式的c语言运行库libc-2.61so。另外还有一个很值得关注的共享对象就是ld-2.6so,它实际上是 linux下的动态链接器。动态链接器与普通共享对象一样被映射到了进程的地址空间,在系统开始运行program1之前,首先会把控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给 program1,然后开始执行。
我们通过 readelf工具来查看 lib. so的装载属性,就如我们在前面查看普通程序一样:
除了文件的类型与普通程序不同以外,其他几乎与普通程序一样。还有有一点比较不同的是,动态链接模块的装载地址是从地址0x00000始的。我们知道这个地址是无效地址,并且从上面的进程虚拟空间分布看到, lib. so的最终装载地址并不是0x000000是0xb7efc000。从这点我们可以推断,共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。
当然,这仅仅是一个推断,至于为什么要这样做,为什么不将每个共享对象在进程中的地址固定,或者在真正的系统中是怎么运作的,我们将在下一节进行解释。