链接:将各种代码和数据片段收集并组合成为一个单一文件的过程,该文件可被加载(复制)到内存并执行。该过程可被执行于编译、加载、运行时,由链接器的程序自动执行。
(编译:源代码被翻译成机器代码;加载:程序被加载器加载到内存并执行;运行:由应用程序来执行。)
7.1编译驱动程序
功能:代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。
作用过程(都是由驱动程序运行完成):
- 预处理器(gcc)将C的源程序main.c翻译成一个ASCLL码的中间文件main.i;
- 编译器(ccl)将main.i翻译成一个ASCLL汇编语言文件main.s(也是可重定位的);
- 汇编器(as)将main.s翻译成一个可重定位目标文件main.o;
- 链接器(ld)将main.o以及其他可重定位目标文件、系统必要的目标文件组合,创建可执行目标文件。
7.2静态链接
功能:以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。
必须完成的两个任务:
- 符号解析:将每个符号引用刚好和一个符号定义联系起来。
- 重定位:链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置。
7.3目标文件
三种形式:
- 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件;
- 可执行目标文件:包含二进制代码和数据,其形式可以被直接复制到内存并执行;
- 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。
7.4可重定位目标文件(由以下节构成)
- ELF头:以一个16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息。
- .text: 已编译程序的机器代码。
- .rodata: 只读数据。
- .data: 已初始化的全局和静态C变量。
- .bss: 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
- .syrntab: 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
- .rel.text: 一个.text文件节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
- .rel.data: 被模块引用或定义的所有全局变最的重定位信息。
- .debug: 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。
- .line:原始C源程序中的行号和 .text节中机器指令之间的映射。(-g运行才会得到。)
- .strtab: 一个字符串表,其内容包括 .symtab和.debug 节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串的序列。
- 节头部表:描述目标文件的节。
7.5符号和符号表
符号种类:
- 由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的函数和全局变量。
- 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应千在其他模块中定义的非静C函数和全局变量。
- 只被模块m定义和引用的局部符号。它们对应于带 static 属性的C函数和全局变量。 这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
每个符号都被分配到目标文件的某个节,由 section 字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节 (pseudosection) , 它们在节头部表中是没有条目的:ABS 代表不该被重定位的符号; UNDEF 代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号; COMMON 表示还未被分配位置的未初始化的数据目标(未初始化的全局变量,不包括初始化为0)。对于 COMMON 符号, value 字段给出对齐要求,而 size 给出最小的大小。注意,只有可重定位目标文件中才有这些伪节,可执行目标文件中是没有的。
7.6符号解析
强弱符号定义:函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
处理多重定义的符号名规则:
- 不允许有多个同名的强符号(强符号只能定义一次)。
- 如果有一个强符号和多个弱符号同名,那么选择强符号。
- 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
静态库:将所有相关的目标模块打包成为一个单独的文件, 可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。
存档文件:静态库以一种称为存档 (archive) 的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置 。存档文件名由后缀 .a 标识。
-static 参数:告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无须更进一步的链接。
-L. 参数:告诉链接器在当前目录下查找紧跟的文件。
一些建议:
- 将库放在命令行的结尾。
- 如果各个库的成员是相互独立(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以按照任何顺序放置在命令行的结尾处。
- 如果库不是相互独立的,那么它们必须排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后的,如果可行,也可将其合并。
易错点:
7.7重定位
组成:
- 重定位节和符号定义:链接器将所有相同类型的节合并为同一类型的新的聚合节。
- 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址器,这一步依赖千可重定位目标模块中称为重定位条目的数据结构。
两种最基本的重定位类型:
- R_X86_64_PC32:重定位一个使用 32 PC 相对地址的引用
- R X86_64_32:重定位一个使用 32 位绝对地址的引用。
7.8可执行目标文件
格式:
- 类似于可重定位目标文件的格式。
- 它还包括程序的入口点 (entry point) , 也就是当程运行时要执行的第一条指令的地址。
- 除了这些节巳经被重定位到它们最终的运行时内存地址以外,.init 节定义了一个小函数,叫做_init, 程序的初始化代码会调用它。
- 可执行文件是完全链接的(已被重定位) 所以它不再需要.rel节。
- ELF 可执行文件被设计得很容易加载到内存,可执行文件的连续的片 (chunk) 被映射到连续的内存段,程序头部表描述了这种映射关系。
7.9加载可执行目标文件
加载:加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程
序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加栽。
特点:
- 每个 Linux 程序都有一个运行时内存映像。
- Linux x86-64 系统中,代码段总是从地址 Ox400000 处开始,后面是数据段。运行时堆在数据段之后,通过调用 malloc 库往上增长。堆后面的区域是为共享模块保留的。用户栈总是从最大的合法用户地址 (248 -1) 开始,向较小内存地址增长。栈上的区域,从地址248开始,是为内核 (kernel) 中的代码和数据保留的。
- .data 段有对齐要求,所以代码段和数据段之间是有间隙的。
- 在分配栈、共享库和堆段运行时地址的时候,链接器还会使用地址空间布局随机化,但它们的相对位置是不变的。
7.10动态链接共享库
静态库缺点:
- 需要定期维护和更新.
- 几乎每个C程序都使用标准IO函数,在运行时,这些函数的代码会被复制到每个运行进程的文本段中,这是对稀缺的内存系统资源的极大浪费。
概念:致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接, 是由动态链接器来执行的。共享库也称为共享目标, Linux 系统中通常用.so后缀来表示。微软的操作系统大量地使用了共享库,它们称为 DLL(动态链接库)。
两种方式:
- 任何给定的文件系统中,对于一个库只有一个.so文件,所有引用该库的可执行目标文件共享这个.so文件中的代码与数据,不是像静态库的内容那样被复制和嵌入到引用它们可执行的文件中。
- 在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。
7.11从应用程序中加载和链接共享库
应用:分发软件与构建高性能Web服务器。
接口:
#include <dlfcn.h>
void *dlopen(const char *filename, int flag); //加载和链接共享库 filename
void *dlsym(void *handle, char *symbol);// 如果symbol存在,就返回符号的地址
//否则返回 NULL
int dlclose (void *handle);// 如果没有其他共享库还在使用这个共享库
//dlclose 数就卸载该共享库
const char *dlerror(void);
// 描述调用 dlopen dlsym 或者 dlclose 函数时发生的最近的错误
//如果没有错误发生,就返回 NULL
7.12位置无关代码
位置无关代码 (Position-Independent Code, PIC):可以加载而无需重定位的代码称。
用户对 GCC 使用 -fpic 选项指示 GNU 编译系统生成 PIC 代码。共享库的编译必须总
使用该选项。
7.13库打桩机制
概念:允许截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,可以追踪对某个特殊库函数的调用次数,验证和追踪它的输入和输出值,或者甚至把它替换成一个完全不同的实现。
1.编译时打桩:linux> gee -I. -o inte int.e mymalloe.o
由于有-I.参数,所以会进行打桩,它告诉C预处理器在搜索通常的系统目录之前,先在当前目录中查找 malloc.h 。
2.链接时打桩:
Linux 静态链接器支持用 --wrap 标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成__wrap_f(前缀是两个下划线),还要把对符号__real_£( 前缀是两个下划线)的引用解析为f。
3.运行时打桩:
构建包含malloc和free包装函数的共享库的方法:
linux> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.e -ldl
编译主程序:
linux> gcc -o intr int.c
7.14处理目标文件的工具
- AR: 创建静态库,插入、删除、列出和提取成员。
- STRINGS: 列出一个目标文件中所有可打印的字符串。
- STRIP: 目标文件中删除符号表信息。
- NM: 列出一个目标文的符号表中定义的符号。
- SIZE: 列出目标文件中节的名字和大小
- READELF: 显示 个目标文件的完整结构,包括 ELF 头中编码的所有信息。包含SIZE和NM 功能。
- OBJDUMP: 所有二进制工具之母。能够显示一个目标文件中所有的信息。它最大的作用是反汇编 .text 节中的二进制指令
- LDD: 列出一个可执行文件在运行时所需要的共享库