至此,我们学习的方向已经从程序与硬件的交换转向了与(系统软件)的交互,而链接可以帮助我们了解系统如何构造我们的程序。

源程序翻译成可执行文件经过四阶段,即 预处理,编译,汇编和链接。预处理与编译相关的内容可以看 这里。本篇博客着重讨论链接问题。

一. 基础概念

  1. 静态链接:

静态链接器以一组可重定位目标文件为输入,生成 完全链接的,可加载运行的可执行目标文件 为输出。链接器应完成两个主要任务:

  • 符号解析:目标文件 定义与引用 符号,每个符号对于一个函数,全局变量或静态变量。符号引用 与 符号定义 应一一关联。

  • 重定位:链接器将每个符号定义 与 一个内存位置关联起来。从而将代码与数据节重定位。链接器修改所有对这些符号的引用并指向这个内存位置。

  1. 目标文件:

目标文件有三个形式:

  • 可重定位目标文件:包含二进制码与数据,不能被执行
  • 可执行目标文件:前者经过链接得到的产物,可被直接复制到内存并执行。
  • 共享目标文件:特殊类型的可重定位目标文件,可在加载或运行时被动态地加载进内存并链接。
  1. 符号:

每个可重定位目标模块 m 都有一个符号表,包含了 m 多定义和引用的符号信息。有三种不同的符号:

  • 全局符号:模块 m 定义且可被其他模块引用,对应与非静态函数与全局变量。
  • 外部符号:其他模块定义且被 m 引用,对于其他函数的非静态函数与全局变量。
  • 局部符号:只在 m 内定义与引用,对应静态(static)属性的函数与全局变量。它们不能被其他模块引用。

局部变量在运行时的栈中被管理,不关链接器啥事。而带有 static 属性的局部变量是受链接器调控的,符号表为其创建唯一的本地链接器符号。

  1. 可重定位目标文件

目标文件是按照对应的格式组织的,对于 Linux 系统,采用的是 ELF(可执行可链接模式),典型的 ELF 大概是这样:

image.png

二. 静态链接器

1. 符号解析:

对于同模块中局部符号的引用,编译器仅允许每个模块的每个局部符号有一个定义,本地链接器对于静态局部变量也确保有唯一的名字;

而对于全局符号,编译器遇到某个非当前模块定义的符号时,会假设该符号在其他模块中被定义,并生成一个链接器符号表条目交给链接器。若链接器找不到这个引用复活的定义,就会终止并报错。

更有甚者,多个目标文件可能会定义相同名字的全局符号,应对该问题必须找到一个合适的规范。在 Linux 编译系统中,编译器向汇编器输出的每个全局符号分类为 强符号弱符号,并隐含地编码在符号表里。

- 强符号:函数以及初始化的全局变量;
- 弱符号:未初始化的全局变量;

根据强弱符号的定义有以下规则:

- 不许有多个同名的强符号。
- 如果有一个强符号与多个弱符号,选择强符号。
- 若多个弱符号重名则随机选择。

如果遇到了以下情况,便可能遇到未曾设想的错误:

image.png

由于 double 类型为八字节,而 int 类型只有四字节;若引用 double 类型的 x,则会覆盖 y 的空间。


2. 重定位:

重定位将合并输入模块,并为每个符号分配运行时地址。

重定位有两步组成: 重定位节与符号定义重定义符号引用。前者使得程序中每条指令和全局变量都有唯一的运行时地址,后者对于代码节与数据节的符号引用进行修改,使其指向正确的运行时地址。

而重定义符号引用依赖于 重定位条目 的数据结构,具体地说,当汇编器生成目标模块时,对于最终位置未知的目标引用,他就会生成一个重定位条目,指导连接器进行 重定义符号引用。

以下述程序为例:

int array[2] = {1,2};
int main()
{
int val = sum(array, 2);
return val;
}

对于上述程序,编译器会产生以下汇编代码:

image.png

其中红字即为调用链接器的提示,并将其存储在目标文件的重定位部分,其中 arraysum 即为全局符号。而 R_X86_64_32 表示使用 32 位绝对寻址 进行重定位,R_X86_64_PC32 表示用 32 位 PC 相对寻址 进行重定位。

其中,当 CPU 执行一条使用 PC 相对寻址的指令时,它就将在指令中编码的 32 位值加上PC 的当前运行时值,得到有效地址(如 call 指令的目标),PC值通常是下一条指令在内存中的地址。

3. 静态库链接

我们都是假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。

实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为 静态库,它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。

这样相比于链接一整个模块来说,减少了可执行文件在磁盘和内存中的大小;且如果需要修改某个函数也不需要编译全部源文件,只需要重新编译那个函数的模块再重新排列好就行了。

image.png

在 Linux 系统中,静态库以一种称为 存档 的特殊文件格式存放在磁盘中。

存档文件是一组连接起来的可重定位目标文件(.o 文件)的集合,有一个头部用来描述每个成员目标文
件的大小和位置。存档文件名由后缀 .a 标识,运行模式如上图。

关于静态库如何通过链接器链接,我懒了。

image.png

image.png

三. 动态链接库

静态链接库的缺点,库函数的代码会被复制到每个运行进程的文本段中,造成内存资源的极大浪费(内存的一个有趣的性质就是,无论系统内存有多大,它总是个稀缺资源,就像磁盘空间和你身旁的垃圾桶)。

共享库可以解决该缺陷。它可以在程序 运行或加载 时,加载到 任意的内存地址,并和一个内存中的程序链接起来,该过程称为 动态链接。在 Linux 中通常用 .so 文件表示,微软通常把共享库称为 DLL。

image.png

一个库只有一个 .so 文件。 所有引用该库的可执行目标文件共享这个文件中的代码和数据,而不是像静态库的内容那样 被复制和嵌入 到引用它们的可执行的文件中。

其次,一个共享库的 text 节的一个副本同样可以被不同的正在运行的进程共享。

四. 库打桩

哈哈哈哈。