《深入理解计算机系统》Chapter 7 读书笔记
链接是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(货被拷贝)到存储器并执行。
链接的时机
- 编译时,也就是在源代码被翻译成机器代码时
- 加载时,也就是在程序被加载器加载到存储器并执行时
- 运行时,由应用程序执行
链接器使分离编译称为可能。
一、编译器驱动程序
大部分编译系统提供编译驱动程序:代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
1.将示例程序从ASCⅡ码源文件翻译成可执行目标文件的步骤
(1)运行C预处理器:源程序main.c->ASCII码中间文件main.i (2)运行C编译器:main.i->ASCII码汇编语言文件main.s (3)运行汇编器:main.s->可重定位目标文件
二、静态链接
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(section)组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为了构造可执行文件,链接器必须完成两个任务:符号解析,重定位
三、目标文件
1.三种形式
- 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
- 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行地被动态地加载到存储器并链接。
四、可重定位目标文件
1.一个典型的ELF可重定位目标文件的格式:
- .text:已编译程序的机器代码。
- .rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
- .data:已初始化的全局C变量。
- .bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
- .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
- .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件结合时,需要修改这些位置。
- .rel.data:被模块引用或定义的任何全局变量的重定位信息。
- .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译驱动程序时才会得到这张表。
- .line:原始C源程序中的行号和.text节中机器指令之间的映射。
- .strtab:一个字符串表,其内容包括:.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
五、符号和符号表
每个可重定位目标模块m都有一个符号表,包含m所定义和引用的符号的信息。
在链接器的上下文中,三种不同的符号:
1.由m定义并能被其他模块引用的全局符号。全局链接器对应于非静态的C函数以及被定义为Cstatic 属性的全局变量。
2.由其他模块定义并被模块m以引用的全局符号——外部符号,对应于定义在其他模块中的C函数和变量
3.只被模块m定义和引用的本地符号。
六、符号解析
1.链接器如何解析多重定义的全局符号
在编译时,编译器向汇编器输出每个全局符号,或者是强或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。
函数和已初始化的全局变量时强符号;
未初始化的全局变量是弱符号。
根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号:
规则1:不允许有多个强符号。
规则2:如果有一个强符号和多个弱符号,那么选择强符号。
规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个。
2.与静态库链接
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库。
3.链接器如何使用静态库来解析引用
- 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器吧f添加到E, 修改U和D来反映f中的符号定义和引用,并继续下一个输入文件。
- 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都反复进行这个过程,直到U和D都不再发生变化。在此时,任何不包含在E中的目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。
- 如果当链接器完成对命令行上输入文件的扫描后,U是非空的,那么链接器就好输出一个错误并终止。否则,它会合并和重定位E中的目标文件,从而构建输出的可执行文件。
七、重定位
一旦链接器完成了符号解析这一步,它就是把代码中的每个符号引用和确定的一个符号定义(即它的一个输入目标模块中的一个符号表条目)联系起来。
重定位由两步组成:
- 重定位节和符号定义。在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。然后,链接器将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每个指令和全局变量都有唯一的运行时存储器地址了。
- 重定位节中的符号引用。在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。为了执行这一步,链接器依赖于称为重定位条目的可重定位目标模块中的数据结构。
1.重定位条目
当汇编器生成一个目标模块时,它并不知道数据和代码最终存放在存储器中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置位置的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。 已初始化的数据的重定位条目放在.rel.data中。
八、可执行目标文件
可执行目标文件的格式类似于可重定位目标文件的格式。ELF头部描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text 、.rodata和.data 节和可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时存储器地址以外。.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位了),所以它不再需要.rel节。
九、加载可执行目标文件
加载器将可执行目标文件中的执行代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载。
要运行可执行目标文件p,可以在Unix外壳的命令行中输入它的名字:
unix> ./p
十、动态链接共享库
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。
共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。微软的操作系统大量地利用了共享库,它们称为DLL(动态链接库)。