对链接的思考与总结

目标文件不能直接执行,它首先需要载入到链接器中。链接器确认main函数为初始进入点(程序开始执行的地方),把符号引用绑定到内存地址,把所有的目标文件集中在一起,再加上库文件,从而产生可执行文件。

如果库函数的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态连接;如果可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们称之为动态链接

收集模块准备执行的三个阶段分别为:链接-编辑,载入,运行时链接。静态链接的模块被链接编辑并载入以便运行。动态链接的模块被链接编辑后载入,并在运行时进行链接以便运行。程序执行时,在main函数被调用之前,运行时载入器把共享的数据对象载入到进程的地址空间。外部函数被真正调用之前,运行时载入器并不解析它们。所以即使链接了函数库,如果并没有实际调用,也不会带来额外的开销。

动态链接的优点非常明显:可执行文件的体积可以非常小。虽然运行速度稍慢一些,但动态链接可以更加有效的利益磁盘空间,而且链接-编辑阶段的时间也会缩短(因为链接器的有些工作被推迟到载入时)

动态链接是从两个分明提高性能:

  • 动态链接可执行文件比功能相同的静态链接可执行文件的体积小。它能够节省磁盘空间和虚拟内存,因为函数库只有在需要时才被映射到进程中。
  • 所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。操作系统内核保证映射到内存中的函数可以被所有使用它们的进程所共享。这很大程度上节省了物理内存,提高系统的整体性能。如果可执行文件时静态链接的,每个文件都将拥有一份函数库的拷贝,显然极其浪费。

动态链接也存在着一个问题,因为程序必须要在运行时能够找到它们所需要的函数库。链接器通过把库文件名或路径名植入可执行文件中来做到这一点。这就意味着,函数库的路径不能随意移动,否则在运行时会导致失败。当在一台机器上完成编译后,把它拿到另一台不同的机器上运行时,可能出现这种情况。执行程序的机器必须具有所有该程序需要链接的函数库,而且这些函数库必须位于在链接器中所说明的目录。不过对于标准库而言这不成问题。

动态链接库的文件名以.so结尾(shared object)。可以使用gcc加 -G 选项来创建。

1
2
3
4
5
6
7
8
9
10
11
//tomato.c
my_lib_function(){
printf("library routine called\n");
}
//test.c
int main(){
my_lib_function();
}
% gcc -o libfruit.so -G tomato.c
% gcc test.c -L/home/chenghuili -R/home/chenghuili -lfruit
% a.out

-L(link)和-R(run)分别告诉链接器在链接时和运行时从哪个目录寻址需要链接的函数库。

使用编译器的-K pic可以为函数库产生与位置无关的代码。与位置无关的代码表示用这种方法产生的代码保证对于任何全局数据的访问都是通过额外的间接方法完成的。这使它很容易对数据进行重新定位,只要简单的修改全局偏移量表的其中一个值就可以。类似的,每个函数调用的产生就像是通过过程链接表的某个间接地址所产生的一样。这样,文本可以很容易的重新定位到任何地方,只要修改一下偏移量表就可以了。所以当代码再运行时被映射进来时,运行时链接可以直接把它们放在任何空闲的地方,而代码本身并不需要修改。

在缺省情况下,编译器不产生与位置无关的代码,因为额外的指针解除引用操作将使程序在运行时变慢。如果不使用与位置无关的代码,所产生的代码就会被对应到固定的地址,这对于可执行文件来说确实很好,但对于共享库速度却要慢一点,因为现在每个全局引用就不得不在运行时通过修改页面安排到固定的位置,这就使得页面无法共享。

根据经验,对于函数库应该始终使用与位置无关代码;对于共享库,与位置无关代码显得格外有用,因为每个使用共享库的进程一般都会把它映射到不同的虚拟地址(尽管共享同一份物理拷贝)


参考资料:
《C专家编程》