静态链接有两大缺陷:
①浪费内存和存储空间。
因为各个可执行文件可能会调用相同的库函数及它们所需要的辅助数据结构。假设有两个目标文件Program1.o和Program2.o,都需要和lib.o进行链接形成可执行文件,当用动态链接的时候内存中只需要存在一份lib.so就可以了。
②模块更新困难。
因为当可执行文件中的一个模块更新之后,所有的模块要重新链接才可以使用。
以Program1.c与Lib.c的动态链接为例如图,
图中将Lib.so与Program1.o并不是真正的进行静态链接,如果在Program1.o中引用了其他模块中的数据或函数,这里假设为foo()。那么链接器会根据foo()的性质做出决定,如果foo()定义于其他静态模块中,那么直接进行静态链接,如果foo()定义于某个动态共享对象中,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,把这个过程留到装载时再进行。
链接器如何知道引用的是一个静态符号还是一个动态符号?此时就要用Lib.so。Lib.so中保存了完整的符号信息,把Lib.so也作为链接的输入文件之一,链接器在解析符号时就可以知道该符号是否是定义在Lib.so中的动态符号。重申一下,这里的Lib.so等名称都是例子中的名称。
①固定装载地址
固定装载地址对于动态链接来说,效果不好,因为在多个模块被多个程序使用的情况下很复杂,要人为的去安排动态库的装载地址。
②装载时重定位
静态链接所用到的重定位是链接时重定位,而这里动态链接可以用装载时重定位,当各模块装载至进程虚拟内存中之后,可以对引用到的符号地址进行重定位。
但是这也带来一个问题,就是动态库中的代码部分所有进程都共享一份,否则就失去了动态链接的意义。当动态链接模块被装载映射至虚拟空间后,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享。
为什么要修改指令?——见下面地址无关代码的㈢
③地址无关代码(PIC)(Position-independent Code)
由于装载时重定位的确定,我们希望模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变。
对于共享文件中的地址引用方式可分为4种:
㈠模块内部的函数调用、跳转等
这种情况下,只要用相对偏移指令就可以了,所以不会改变代码。
㈡模块内部的数据访问,比如模块中定义的全局变量、静态变量
模块内部的数据访问依然可以用相对寻址(指令中不能包含数据的绝对地址),虽然现代的体系结构中,数据的相对寻址往往没有相对与当前指令(PC)的寻址方式,但是还是有很多方法得到PC值之后加一个偏移进行对数据的寻址的。
㈢模块间数据访问
因为模块间的数据访问目标地址要等到装载时才能确定。地址无关代码的基本思想就是把跟地址相关的部分放到数据段里面。ELF的做法就是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table,GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用,基本机制如下图:
㈣模块间调用、跳转
对于模块间的调用和跳转也可以运用GOT来解决,不同的是GOT中存储的目标函数的地址。
动态链接比静态链接要灵活得多,但它是以牺牲一部分性能为代价的(装载后链接的性能)。在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或用户很少使用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费,所以采用一种叫做延迟绑定(Lazy Binding)的技术,基本思想就是当函数第一次被用到时才进行绑定(符号查找,重定位等)。而这些绑定是由动态链接器来负责的,所以在加载的时候动态链接器是加载到进程虚拟地址空间中的。
ELF使用PLT(Procedure Linkage Table)来实现,具体就不展开了。