ELF动态链接
为什么要使用动态链接?
在现代的linux系统中,假设一个普通的程序会使用到c语言静态库至少1MB以上,那么,如果我们的机器运行100个这样的程序,就用浪费近100MB的内存;如果磁盘有2000个这样的程序,就要浪费2GB的内存。
静态链接对程序的更新、发布等也会带来问题。比如程序program1使用由第三方厂家提供的库lib.o,当厂家更新lib.o时,程序program1的厂商必须先得到lib.o,那后将program1重新链接,并将整个新的program1发布给客户。可以想象,如果一个这个程序很大,这是多么耗费时间的事。
动态链接正是为了解决静态链接而出现的一种技术。它的优点是:共享的目标文件在磁盘中只有一个副本,在内存中的代码段只有一个副本(通过文件内存映射来实现),这样不仅节约了内存空间,还可以减少物理页面的换进换出;可以动态更换某个模块而不需要重新编译整个程序,不过要重新启动程序才行。这样使得程序升级很方便。
动态链接的基本思想
动态链接的基本思想就是把程序模块拆分成几个相对独立的部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不像静态链接把所有程序模块都链接成一个单独的可执行文件。Linux系统中,ELF动态链接文件被为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,一般以.so为扩展名。
由于共享对象是可以同时被很多进程共享的,所以我们不应该在编译共享对象的时候就把共享对象中的地址确定下来。如果确定下来了,由于每个进程的虚拟地址空间都不同,这样很难避免共享对象的地址空间和进程的其它地址空间冲突。
如果不能在编译共享对象时确定共享对象的地址,能否在共享对象装载时确定呢?装载时重定位看起来好像是可以解决共享对象装载到任意虚拟地址空间的问题,都是在编译时目标地址不确定而需要在装载是将模块重定位。但是装载时重定位还是不适合用来解决共享对象所存在的问题。想象下,动态链接模块被装载到内存且被映射到虚拟地址空间后,指令部分是在多个进程之间共享的,由于装载时重定位的方法是需要修改指令的,所以没有办法做到同一份指令被多个进程所共享。当然,动态链接库中的可修改数据部分对于不同的进程来说有多个副本,所以它们可以采用装载时重定位的方法来解决。
我们的目的很简单,就是希望程序模块中共享的指令在装载时不需要因为装载地址的改变而改变,所以实现的基本思想就是把指令中需要修改的部分分离出来,跟数据部分存放在一起,这样指令部分就可以保持不变了,而数据部分可以每个进程一个副本。这种技术就是PIC技术(Position-Independent Code)。ELF的具体做法是在数据段中建立一个指向其它模块变量地址的指针数组,称为全局偏移表(Global Offset Table,GOT),而代码中引用全局变量的代码都改成引用GOT。当代码中需要引用该全局变量时,就可以通过GOT相应项间接引用。
动态链接下,程序模块之间包含了大量的函数引用(全局变量往往比较少,因为大量的全局变量引用会导致模块之间的耦合很大),所以在程序开始运行时,会消耗大量的时间来解决模块间函数引用的符号查找和重定位。实际上有很多的函数是不会调用的,如错误处理函数和用户很少使用的功能模块,一开始绑定就浪费资源和时间了。所以ELF采用了一种叫做延迟绑定的做法,基本思想就是当函数第一次被用到时才进行绑定,如果没用到就不绑定。ELF使用PLT(Procedure linkage table)的方法来实现延迟绑定。编译器将GOT分成GOT和GOT.PLT表,GOT用来保存全局变量的地址,GOT.PLT用来保存函数引用的地址。为了实现延迟绑定,我们就不能直接访问GOT表来获取目标函数的地址,必须又添加一层中间跳转表,这个中间跳转表叫做PLT表,由PLT表来真正完成绑定的工作。