静态链接和动态链接
前面的章节一直使用静态链接,因为它的模型最简单,也最适合初学者建立底层直觉。本章开始比较静态链接和动态链接。
静态链接把依赖代码直接放进可执行文件,理解起来直观;但现实系统中,大量程序默认采用动态链接。理解二者差异,是理解现代可执行文件、共享库和链接错误的关键。
本章先讲静态链接如何把目标文件和库打包成一个文件,再讲动态链接为什么需要位置无关代码、共享库和运行时加载器。
现代 Linux 发行版中,编译器和链接器通常默认生成动态链接程序。如果之前忘记给 cc 加 -static,很可能会看到类似下面的错误。想复现的话,可以从 Makefile 里暂时去掉 -static 再执行 make。
$ cc -o tmp tmp.s
/usr/bin/ld: /tmp/ccaRuuub.o: relocation R_X86_64_32S against `.data' can not be used when making a PIE object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output根本原因是:动态链接对汇编代码有额外要求,而当前的 9cc 还没有生成这类代码的能力。
因此,一旦不再强制静态链接,链接器就会报错。本章会把这个错误背后的机制讲清楚。
静态链接
静态链接的可执行文件在运行时不需要其他文件,是一个自包含的可执行文件。
例如 printf 不是用户自己写的函数,而是 libc 标准库中的函数。制作静态链接可执行文件时,printf 的代码会从 libc 复制进可执行文件。静态链接程序运行时不需要 libc,因为 libc 中必要的代码和数据已经被复制到了可执行文件中。
这里实际看一个简单程序 hello.c 如何变成静态链接可执行文件。
#include <stdio.h>
int main() {
printf("Hello world!\n");
}
为了把这个 hello.c 编译并链接成名为 hello 的可执行文件,输入下面的命令。
$ cc -c hello.c
$ cc -o hello hello.o上面的命令中,第 1 行把 hello.c 编译成目标文件 hello.o,第 2 行对其执行链接并生成可执行文件。
这两条命令也可以写成 cc -o hello hello.c;那样启动编译器时,内部也会执行与上述两步等价的处理。
hello.c 包含了 stdio.h。如本书之前所见,头文件并不包含函数本体代码。因此生成 hello.o 时,编译器通过 stdio.h 中的声明知道 printf 函数的存在和类型,但并不知道 printf 的实际代码。因此 printf 的代码不包含在 hello.o 中。实际上 hello.o 只包含 main 的定义。把 hello.o 和包含 printf 的目标文件组合成可执行文件,是链接器的职责。
第 2 行通过 cc 启动链接器时,传给链接器的不只是命令行中的 hello.o,还会传入系统标准库路径下的 /usr/lib/x86_64-linux-gnu/libc.a。printf 函数包含在这个 libc.a 中。.a 文件和 .tar、.zip 一样,是一种归档文件。可以稍微看一下里面的内容。
$ ar t /usr/lib/x86_64-linux-gnu/libc.a
...
printf_size.o
fprintf.o
printf.o
snprintf.o
sprintf.o
...musl 的 printf.c
归档文件中保存着多个目标文件。链接器只会从归档中取出当前程序实际需要的目标文件,并把它们链接进最终的可执行文件。
静态链接可执行文件的运行模型很简单。运行时内存中还没有该可执行文件,因此可执行文件的各个段可以加载到内存中的任意位置。只要能加载到链接时决定的默认地址,就不会失败。由于加载可执行文件前内存中没有其他片段需要配合,静态链接可以在链接时确定所有全局变量和函数的地址。
静态链接有以下优点。
- 加载简单而且速度快。
- 不依赖其他文件,只要复制可执行文件就能运行。
- 即使库的版本在环境之间存在细微差异,静态链接也会把库的代码和数据固定复制进可执行文件,因此同一个可执行文件在任何环境中都会以相同方式运行。
静态链接也有以下缺点。
- 可执行文件会复制所使用库函数的代码,在磁盘和内存上多少会造成浪费。
- 库修复缺陷时,为了让已有可执行文件反映该修改,需要重新链接。
另一方面,动态链接的可执行文件在运行时需要其他文件,例如 Unix 上的 .so,Windows 上的 .dll。.so 和 .dll 中包含 printf 等函数代码和 errno 这样的全局变量。这类文件称为动态库、共享库,或 DSO(dynamic shared object)。