《深入理解计算机系统》第7章 链接
本章主要内容:
-
链接——静态链接、动态链接(链接又包括两个主要任务:符号解析和重定位)
-
符号——全局符号和本地符号、符号表、符号解析
-
链接文件的创建及引用——gcc、ar rcs、sharedj及fPIC命令参数
-
重定位——重定位条目、重定位符号引用(PC相对引用和绝对引用)
-
目标文件——可重定位目标文件(其中又详细介绍了ELF可重定位文件的结构及格式)、可执行目标文件、共享目标文件
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。 链接可以执行于编译时,即源代码被翻译成机器代码时;也可执行于加载时,即在程序被加载器加载到存储器并执行时;甚至执行于运行时,由应用程序来执行。
1.编译器驱动程序
大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
GNU编译系统编译源码:
- 首先,运行C预处理器(cpp),将.c文件翻译成.i文件;
- 接着,运行C编译器(cc1),将.i文件翻译成ASCII汇编语言文件.s文件;
- 然后,运行汇编器(as),将.s文件翻译成可重定位目标文件.o文件;
- 最后,运行链接器(ld),将各.o文件组合起来,创建一个可执行目标文件。
2.静态链接
Unix的静态链接器(static linker)ld,以一组可重位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(section)组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为了构造可执行文件,链接器必须完成两个主要任务:
- 符号解析(symbol resolution)。目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
- 重定位(relocation)。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
3.目标文件
目标文件有三种形式:可重定位目标文件。可以在编译时与其它可重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件。可被直接拷贝到存储器并执行。
- 共享目标文件。在加载或运行时被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。
现代Unix系统使用可执行和可链接格式(ELF)。
可重定位目标文件
一个典型的可重定位目标文件包含下面几个节:
.text:已编译程序的机器代码。
.rodata:只读数据。
.data:已初始化的全局C变量。局部C变量在运行时保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局C变量。
4. 符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。
-
在链接器的上下文中,有三种不同的符号:
- 1、由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数以及被定义为不带C static属性的全局变量。
- 2、由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号(external),对应于定义在其他模块中的C函数和变量。
- 3、只被模块m定义和引用的本地符号。有的本地链接器符号对应于带static属性的C函数和全局变量。
-
符号表
-
- 每个符号都和目标的某个节相关联,由section字段表示。
- section字段三个特殊的伪节
- ABS:不该被重定位的符号。
- UNDEF:未定义的符号,在本目标模块中引用,但在其他地方定义。
- COMMON:未被分配位置的未初始化数据目标。
- Ndx=1表示.test节,Ndx=3表示.data节。
5. 符号解析
-
多重定义的全局符号
- 强符号:函数和已经初始化的全局变量
- 弱符号:未初始化的全局变量
- 规则:
规则1:不允许有多个强符号。 规则2:如果有一个强符号和多个弱符号,那么选择强符号。 规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个。
-
静态库链接
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(Linux下是存档文件,Windows下是lib),可以用做链接器的输入。
-
- 当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
- 存档文件:一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
- 链接时加上-static参数:告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并执行,在加载时无需更进一步的链接。
6. 重定位
-
重定位节和符号定义:
- 链接器将所有相同类型的节合并为同一类型的新的聚合节,将运行时存储器地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
- 此时,程序中的每个指令和全局变量都有唯一的运行时存储器地址了。
-
重定位节中的符号引用:
- 链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
- 链接器依赖于称为重定位条目的可重定位目标模块中的数据结构。
-
重定位符号引用
- 相对引用
- 绝对引用
7. 可执行目标文件及加载
(1)可执行目标文件
- C程序开始时是一组ASCII文本文件,已经被转化为一个二进制文件,且这个二进制文件包含加载程序到存储器并运行它所需的所有信息。
- 段头部表:可执行文件的连续片被映射到连续的存储器段,段头部表描述了这种关系。
(2)加载可执行目标文件
加载器将可执行目标文件中的执行代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序拷贝到存储器并运行的过程叫做加载。
Unix程序运行时存储器映像:
- 用户栈总是最大的合法用户地址开始,向下增长的(向低存储器地址方向增长)。从栈的上部开始的段是为操作系统驻留存储器的部分(也就是内核)的代码和数据保留的。
- 当加载器运行时,它创建如上图所示的存储器映像。在可执行文件中段头部表的指导下,加载器将可执行文件的相关内容拷贝到代码和数据段。
- 接下来,加载器跳转到程序的入口点,也就是符号_start的地址。在_start地址处的启动代码(startup code)是在目标文件ctrl.o中定义的,对所有的C程序都是一样的。
8. 动态连接共享库
- 共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。
- 共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。微软的操作系统大量地利用了共享库,它们称为DLL(动态链接库)。
- 共享库是以两种不同的方式来“共享”的(在Windows中分别称为“隐式链接”和“显示链接”)。
- 首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌入引用它们的可执行的文件中。
- 其次,在存储器中,一个共享库的.text节 一个副本可以被不同的正在运行的进程共享。
-
与位置无关的代码PIC
编译库代码,使得不需要链接器修改库代码就可以在任何地址加载和执行这些代码。
-
- 用户对GCC使用
-fPIC
选项指示GNU生成PIC代码
- 用户对GCC使用
9. 处理目标文件的工具
- AR:创建静态库,插入、删除、列出和提取成员。
- READELF:显示一个目标文件的完整结构,包括ELF头中的编码的所有信息。包含SIZE和NM的功能。
- OBJDUMP:所有二进制工具之母,能够显示一个目标文件中所有的信息。它最大的作用是反汇编.text节中的二进制指令。
- LDD:列出一个可执行文件在运行时所需要的共享库。
- STRINGS:列出一个目标文件中所有可打印的字符串。
- STRIP:从目标文件中删除符号的信息。
- NM:列出一个目标文件的符号表中定义的符号。
- SIZE:目标文件中节的名字和大小。
时间: 2024-10-22 18:36:09