本文主要是《程序员的自我修养》一书的内容摘要和梳理,如有需要并且没有被本文涵盖的内容,建议读者自行观看原书。
基于前面两部分的内容,分别介绍了程序编译的基本流程,及目标文件结构的相关内容,本文开始介绍静态链接的过程。
静态链接——编译和链接(一)
静态链接——目标文件(二)
装载与动态链接——装载与进程(一)
装载与动态链接——动态链接(二)
空间与地址分配
前面已经介绍了目标文件都是由不同的 section 组成的,本小节主要是介绍链接器是如何将不同目标文件的各个段合并在一起,最终组成一个输出文件的。
最简单的就是直接按照顺序将它们单纯叠加就行了,这么做一个主要问题就是由于内存需要对齐,可能会造成大量的内存空间浪费。
链接器实际是会对不同目标文件中的相似段合并。链接器会扫描所有输入的目标文件,获取并收集它们的信息,统一放到全局符号表,计算合并后对应段的长度和位置,建立映射关系,映射之后程序所使用的地址就是虚拟地址了,各个段的 VMA 也会有对应的虚拟地址信息。

在确定各个段的虚拟地址之后,连接器就开始计算各个符号的虚拟地址,最后将各个符号的虚拟地址更新到符号表中。
符号解析与重定位
经过上面的空间与地址分配之后,每个符号的虚拟地址都确定了,但是引用的外部符号在代码指令中还是需要地址重定位的,用来更正到正确的虚拟地址,本质是将相对地址转化程绝对地址,具体地址修正方法可以参考原书“指针修正”一节。
对于需要重定位的指令,在目标文件中也是会有一个专门的重定位表(Relocation Table)的结构保存这些与重定位相关的信息。
重定位的过程就伴随符号解析的过程,每个目标文件都可能定义一些符号,也可能引用到定在其他目标文件的符号,链接器需要对某个符号重定位时就需要确定这个符号的目标地址。这时候链接器会去查找所有输入目标文件的符号表组成的全局符号表,找到以后进行重定位。
定义在其他目标文件符号会在代码段中被定义为 undefined 类型,这种未定义的符号是因为该目标文件有关于它们的重定位项,当链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能在全局符号表中找到,否则链接器就会报我们熟悉的为定义错误:
undefined reference to "xxxx"
静态库链接
进一步的,静态链接以后我们可以得到一个目标文件,而静态库则是可以看作是一组目标文件的集合,即多个目标文件经过压缩以后形成的一个文件。例如 c 语言静态库 lib.a。
因为把文件操作 fread.o,fwrite.o,内存管理 malloc.o 等零散的目标文件直接提供给库的使用者,会造成文件传输、管理和组织方面的很多不便,所以通常通过 “ar” 压缩程序将这些目标文件压缩到一起,并进行编号和索引,以便查找和检索。
并且静态库里面的每个目标文件一般就只包含一个函数,主要是为了避免在引用一个需要的函数时把其他不用的函数也都链接到输出的目标文件里,运行库有成百上千的函数,数量很庞大,每个函数独立可以尽量减少看空间的浪费。
C++ 与 ABI
这里单独把这一小节单拎出来是因为在实际工程项目中,会遇到 abi 版本的问题,既然都读了 《程序员的自我修养》 了,再对相关的名次没啥概念可就说不过了,所以单独给它一小节 😃。
API(Application Programming Interface) 是应用程序接口,我们也是经常提到的,API 往往指源码级别的接口,而 ABI(Application Binary Interface) 是指二进制层面的接口,ABI 的兼容比 API 要更严格,API 相同不代表 ABI 也相同。
以 c 语言为例,下面的几个方面会决定目标文件之间是否二进制兼容:
- 内置类型的大小和存储方式(大小段,内存对齐方式)
- 组合类型的存储方式和内存分布
- 外部符号与用户定义符号之间的命名方式和解析方式
- 函数调用方式,如参数入栈顺序、返回值如何保持等
- 堆栈的分布方式,如参数和局部变量在堆栈的位置、参数传递方法等
- 寄存器使用约定
c++ 时代,由于其复杂的特性 ABI 兼容又增加了很多额外的内容: - 继承类体系的分布
- 指向成员函数的指针的内存分布
- 如何调用虚函数
- template 如何实例化
- 外部符号修饰
- 全局对象的构造和析构
- 异常的产生和捕获机制
- 标准库的细节问题
- 内嵌函数访问细节
目前 ABI 的兼容问题还是没有被彻底解决。
原文如下:
由于现实问题,这个问题还是会长期地存在,这也是为什么这么多像我们这样的程序员能够存在的原因。
本文详细解析了静态链接过程中的空间分配、地址解析与重定位,涉及静态库链接及C++ ABI的兼容性问题,探讨了内存对齐、全局符号表、重定位表在链接中的作用,以及C++中API与ABI的区别和挑战。
1059





