静态链接

《程序员的自我修养——链接、装载与库》读书笔记

        静态链接要解决的问题是将几个目标文件链接起来成为形成一个可执行文件。现在的链接器一般都采用一种叫做两步链接(Two-pass Linking)的方法。

  • 第一步,地址与空间分配,扫描所有的输入目标文件,合并它们各个节,更新节表和全局符号表。
  • 第二步,符号解析与重定位,利用上一步搜集到的信息,读取文件中节的数据、重定位信息,进行符号解析与重定位,调整代码中的地址等。

1. 空间和地址的分配

        链接器会为目标文件分配地址和空间,这里不仅是指在输出的可执行文件中的空间,也是在装载后的虚拟地址中的虚拟地址空间。但像.bss这样的节来说,分配空间的意义只限于虚拟地址空间。用objdump -h可以看到链接前后的虚拟地址的分配情况,其中VMA表示虚拟地址(Virtual Memory Address),LMA表示加载地址(Load Memory Address),正常情况下这两个值是一样的。


静态链接前
图1 静态链接前


静态链接后
图2 静态链接后

        链接前,目标文件中的所有节的VMA都是0,此时虚拟空间还没有被分配。等到链接后,可执行文件中的各个节都被分配到了相应的虚拟地址。

        输入文件中的各个节的虚拟地址确定以后,链接器开始计算各个符号的虚拟地址。因为各个符号在节内的相对位置是固定的,它们的地址也已经是确定的了,也就是节的虚拟首地址加上该符号在节内的偏移量。这样链接器就可以更新全局符号表了。

2. 符号解析与重定位

        在ELF文件中有一个叫重定位表(Relocation Table)的结构,专门用来保存这些与重定位相关的信息,它在ELF文件中往往是一个或多个节。对于每个要重定位的ELF节都有一个对应的重定位表,占据一个节,所以也可以叫重定位节。比如,.rel.text节保存了代码节.text的重定位表,.rel.data节保存了.data的重定位表。objdump -r指令可以用来查看目标文件的重定位表。每个要被重定位的地方叫做一个重定位入口(Relocation Entry),重定位入口的偏移(Offset)表示该入口在要被重定位的节中的位置。

        不同机器指令的寻址方式可能不同,这样导致重定位时的修正方式也是不同的。举个例子,采用绝对寻址的指令,重定位的修正方式为保存在被修改位置的值 + 符号的实际地址,采用相对寻址的指令的修正方式为保存在被修改位置的值 + 符号的实际地址 - 被修正的位置的地址

3. 强、弱符号和强、弱引用

        当多个目标文件中含有相同名字的全局符号的定义时,这些目标文件在链接时将会出现符号重定义的错误。这种符号可以被称为强符号(Strong Symbol)。当然还有一些符号的定义可以被称为弱符号(Weak Symbol)。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。GCC的__attribute__((weak))可以用来定义任何一个强符号为弱符号。强符号和弱符号都是针对符号的定义来说的,而不是符号的引用。针对强弱符号的概念,链接器会按照如下规则处理:

  1. 不允许强符号被多次定义,否则会报重定义错误
  2. 如果一个符号在某个目标文件中为强符号,在其它文件中都为弱符号,那么选择强符号
  3. 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个

        在目标文件中对外部目标文件的符号引用在链接过程中也要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种引用被称为强引用(Strong Reference),与之相应的还有一种弱引用(Week Reference)。在处理弱引用时,如果该符号未被定义,则链接器对该引用不报错,默认其为0或者某个特殊的值,以便于程序代码能够识别。在GCC中可以使用__attribute((weakref))__这个扩展关键字来声明一个引用为弱引用。

        这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使程序可以使用自定义版本的库函数;或者程序可以对某些扩展模功能模块的引用定义为弱引用,当我们的扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序更加容易剪裁和组合。

4. COMMON块

        在前面ELF文件的基本结构中,我们说未初始化的全局变量和局部静态变量存储在.bss节中,其实现在的编译器生成的目标文件中,未初始化的全局变量往往并没有被放在.bss节中。在我们了解了强弱符号和强弱符号的相关知识后,可以来探讨这个问题了。

        现在的编译器和链接器都支持一种叫做COMMON块(Common Block)的机制,这种机制最早来源于Fortran,早期的Fortran没有动态分配空间的机制,程序员必须事先声明他所需要的临时使用空间的大小。Fortran把这种空间叫做COMMON块,当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那一块为准。

        现代的链接机制在处理弱符号的时候,采用的就是和COMMON块一样的机制。当有一强符号时,最终输出结果中的符号所占空间与强符号的相同,但若有弱符号的大小大于强符号,链接器会输出警告。导致需要COMMON块这种机制的根本原因是链接器不支持符号类型,也就无法判断各符号的类型是否一致。

        当编译器将一个编译单元编译成目标文件时,如果该编译单元中包含了弱符号,那么该弱符号最终所占的空间大小此时是未知的,也就不能为它在.bss节分配空间,只有链接器在链接的时候才能确定该弱符号的大小,它可以在最终输出文件的.bss节为其分配空间。所以总体看来,未初始化的全局变量最终还是被放在.bss节中的。GCC的编译选项-fno-common和扩展关键字__attribute__((nocommon))允许我们将未初始化的全局变量不以COMMON块的形式处理,那么它就相当于一个强符号了。

6. 静态链接库

        静态链接库实际上是一组目标文件的集合,即很多目标文件压缩打包后形成的一个文件。gcc在执行静态链接时,会自动找到我们的目标文件所引用的目标文件所在的和所依赖的静态链接库,并把它们链接进来。为了减小空间的浪费,静态链接库中的每个目标文件往往只包含一个函数。

7. 总结

        这里描述的就是链接的基本过程,但是静态链接存在着很多弊端,比如可执行文件变得很大,库文件更新不方便等等,现在的程序大多都使用的动态链接的方式,后面继续展开。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值