CSAPP 读书笔记-第七章链接
7.1 编译器驱动程序
大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。比如,要用 GNU 编译系统构造示例程序,我们就要通过在 shell 中输入下列命令来调用 GCC 驱动程序:
1 | gcc -Og -o prog main.c sum.c |
驱动程序首先运行 C 预处理器(cpp),它将 C 的源程序 main.c
翻译成一个 ASCII 码的中间文件 main.i
;
接下来,驱动程序运行 C 编译器(ccl),它将 main.i
翻译成一个 ASCII 汇编语言文件 main.s
;
然后,驱动程序运行汇编器(as),它将 main.s
翻译成一个可重定位目标文件(relocatable object file)main.o
;
最后,驱动程序运行链接器程序 ld,将 main.o
和 sum.o
以及一些必要的系统目标文件组合起来,创建一个可执行目标文件(executable object file)prog
。
7.2 静态链接
为了构造可执行文件,链接器必须完成两个主要任务:
- 符号解析(symbol resolution)。目标文件定义和引用符号,每个符号对应于一个函数、全局变量、或静态变量。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
- 重定位(relocation)。编译器和汇编器生成从地址 0 开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
7.3 目标文件
目标文件有三种形式:
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
- Windows 使用可移植可执行(Portable Executable,PE)格式。
- MacOS 使用 Mach-O 格式。
- 现代 x86-64 Linux 和 Unix 系统使用可执行可链接格式(Executable and Linkable Format,ELF)。
7.6 符号解析
C++ 中链接器符号的重整:编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字。这种编码过程叫做重整(mangling),而相反的过程叫做恢复(demangling)。一个被重整的类名是由名字中字符的整数数量,后面跟原始名字组成的。比如类 Foo
被编码成 3Foo
。方法被编码为原始方法名,后面加上,加上被重整的类名,再加上每个参数的单字母编码。比如 Foo::bar(int, long)
被编码为 bar3Fooil。
7.6.1 链接器如何解析多重定义的全局符号
函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
解析规则:
- 规则 1:不允许有多个同名的强符号。
- 规则 2:如果有一个强符号和多个弱符号同名,那么选择强符号。
- 规则 3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
7.6.2 与静态库链接
当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。
在 Linux 系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件名由后缀 .a
标识。
7.6.3 链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。
在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。
关于库的一般准则时将它们放在命令行的结尾。
如果需要满足依赖需求,可以在命令行上重复库。
7.7 重定位
重定位由两步组成:
- 重定位节和符号定义。链接器将所有相同类型的节合并为同一类型的新的聚合节;然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
- 重定位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
7.12 位置无关代码
可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)。用户对 GCC 使用 -fpic
选项指示 GNU 编译系统生成 PIC 代码。共享库的编译必须总是使用该选项。
7.13 库打桩机制
库打桩(library interpositioning)允许你截获对共享库函数的调用,取而代之执行自己的代码。
库打桩的基本思想:给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。包装函数通常会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
打桩可以发生在编译时、连接时或当程序被加载和执行的运行时。
7.13.1 编译时打桩
使用 C 预处理器在编译时打桩。
7.13.2 链接时打桩
Linux 静态链接器支持用 --wrap f
标志进行链接时打桩。这个标志告诉链接器,把对符号 f
的引用解析成 __wrap_f
,还要把对符号 __real_f
的引用解析为 f
。
7.13.3 运行时打桩
编译时打桩需要能够访问程序的源代码,链接时打桩需要能够访问程序的可重定位对象文件。运行时打桩只需要能够访问可执行目标文件。
运行时打桩基于动态链接器的 LD_PRELOAD
环境变量。
如果 LD_PRELOAD
环境变量被设置为一个共享库路径名的列表,那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器会先搜索 LD_PRELOAD
库,然后才搜索任何其他的库。
7.14 处理目标文件的工具
- AR:创建静态库,插入、删除、列出和提取成员。
- STRINGS:列出一个目标文件中所有可打印的字符串。
- NM:列出一个目标文件的符号表中定义的符号。
- SIZE:列出目标文件中节的名字和大小。
- READELF:显示一个目标文件的完整结构,包括 ELF 头中编码的所有信息。包含 SIZE 和 NM 的功能。
- OBJDUMP:所有二进制工具之母。能够显示一个目标文件中所有的信息。它最大的作用是反汇编
.text
节中的二进制指令。 - LDD:列出一个可执行文件在运行时所需要的共享库。