文章目录
链接
编译器驱动程序
-
预处理:预处理器将.c文件翻译到.i文件(ASCII码的中间文件)
-
编译:编译器将.i文件翻译成到.s文件(ASCII汇编语言文件)
-
汇编:汇编器将.s文件翻译到到.o文件(可重定位目标文件)
静态链接
- 概念:链接器(static linker)以一组可重定位目标文件和命令行参数作为输入,输出一个完全链接的,可以加载和运行的可执行目标文件
- 任务:符号解析、重定位
目标文件
三种形式
- 可重定位目标文件:包含二进制代码和数据,在编译时与其他可重定位目标文件联合形成一个目标文件
- 可执行目标文件:包含二进制代码和数据,可以直接被复制到内存并执行
- 共享目标文件:一种特殊的可重定位目标文件目标文件,可以在加载或者运行时被动态的加载到内存并链接
格式
- Windows下的PE格式:可移植可执行(Portable Executable)
- Linux/Unix下的ELF格式:可执行可链接格式(Executable and Linkable Format)
可重定位目标文件
ELF头
以一个16字节的描述了生成该文件的系统的字的大小和字节顺序信息的序列开始,剩下的部分包含帮助连接器语法分析和解释目标文件的信息,如
- ELF头的大小
- 目标文件类型(三种形式中的哪一个)
- 机器类型
- 节头部表的文件偏移(在本文件的偏移量)以及节头部表中的条目大小与数量
节
存储各种信息
- .text:code
- .rodata:read only data
- .data:已初始化的全局变量和静态变量(局部变量是运行时被栈管理的)
- .bss:未初始化的(默认为0)以及被初始化为0的全局变量和静态变量
- .symtab:符号表,函数和全局变量信息
- .rel.text:一个.text节中位置的列表,当重定位时这些位置需要修改
- .rel.data:被模块引用或者定义的所有全局变量的重定位信息(寻址重定位)
- .debug:调试符号表
- .line:原始c源程序中的行号和.text节中机器指令的映射
- .strtab:字符串表,以null结尾的字符串序列。(.symtab表中的符号的名字保存在这里)
节头部表
描述每个节的位置和大小,每个节都在节头部表中有一个条目
符号和符号表
每个可重定位目标模块 m 都有一个符号表,包含 m 定义和引用的符号的信息
三种符号类型:
- 由m定义并能被其他模块引用的全局符号,即非static全局变量和函数
- 由其他模块定义并被m引用的全局变量,称为外部符号
- 只被m定义和引用的局部符号,即static变量和函数。
局部符号和局部变量是不同的,局部变量在栈中
符号表
符号表中的条目格式:
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
binding:4; /* Local or global (4 bits) */
char reserved; /* Unused */
short section; /* Section header index */
long value; /* Section offset(可重定位目标文件) or absolute(可执行目标文件) address */
long size; /* Object size in bytes */
} Elf64_Symbol;
每个符号都被分配到某个节中
可重定位目标文件中的三个伪节
在节头部表是没有条目的
- ABS:不该被重定位的符号
- UNDEF:未定义的符号
- COMMON:还未被分配位置的未初始化的目标数据(链接阶段会被分配位置或舍弃)
- value给出对齐目标
- size给出最初大小
- 与.bss区别很小(这么分的原因在下一节)
- COMMON:未初始化的全局变量
- .bss:未初始化的静态变量以及初始化为零的全局或静态变量
符号解析:
对于局部符号(引用和定义在同一模块),编译器可以确保每个符号仅有一个唯一的名字和定义,从而完成符号解析
多个目标文件可能会定义同名的全局符号,而且会引用不在本模块定义的全局符号,所以符号解析由连接器完成
链接器如何解析多重定义的全局符号
编译时,编译器向汇编器输出每个全局符号,或者是强符号或者是弱符号,汇编器把这个信息隐含的编码在可重定位目标文件的符号表中
- 未初始化的全局变量是弱符号
- 函数和已初始化的全局变量是强符号
Linux链接器根据以下规则来处理多重定义的符号名:
- 不允许有多个同名的强符号
- 如果一个强符号和多个弱符号同名,选择强符号
- 如果有多个弱符号同名,任意选一个
对于未初始化的全局变量,编译器不知道是其他模块是否有同名弱符号,也不知道链接器会使用多重定义中哪一个,所以放在COMMON段让链接器决定,而静态符号的构造是唯一的,初始化为0的全局变量是强符号,编译器都能确定,所以分配在确定的节中,而不是伪节
与静态库的链接
将所有目标文件打包成为一个单独的文件称为静态库(static library),可供链接器只复制被程序引用的模块
在Linux中以一种称为存档的特殊文件格式存储在磁盘中,以后缀 .a 来标识
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7gSOkNOd-1626579806996)(C:\Users\吴鑫磊\AppData\Roaming\Typora\typora-user-images\image-20210718095205799.png)]
链接器如何使用静态库来解析引用
在符号解析阶段,链接器从左到右按照在命令行上的顺序扫描可重定位目标文件和存档文件(库),扫描中链接器会维护:
- 可重定位目标文件的集合E(最终被合并生成可执行文件)
- 未解析的符号集合U(被引用了但没有定义)
- 前面已经输入的文件中已定义的符号集合D
对于可重定位目标文件,将所有符号放入U或者D,对于库文件,查找U中的符号的定义,没用到的都抛弃,所以命令行上库的顺序很重要
重定位
完成了符号解析,连接器就知道了输入目标模块的代码节和数据节的确切大小,可以开始重定位,为每个符号分配运行时地址了
步骤:
- 重定位节和符号定义:比如将所有输入模块的.data节合并,然后将运行时地址赋给新的节,赋给每个符号,完成时程序中的每条指令和全局变量都有唯一的运行时内存地址了
- 重定位条目中的符号引用:根据重定位条目修改代码节和数据节中每个符号引用使他们指向正确的运行地址
重定位条目
当汇编器生成一个目标模块时,当它遇到最终未知的目标引用,就会生成一个重定位条目告诉链接器如何修改这个引用代码的重定位条目在.rel.text中,数据的在.rel.data中
typedef struct {
long offset; /* Offset of the reference to relocate */需要被修改的引用的节偏移
long type:32, /* Relocation type */ 告诉链接器怎么修改
symbol:32; /* Symbol table index */ 被修改引用应该指向的符号
long addend; /* Constant part of relocation expression */
} Elf64_Rela;
ELF定义了32中不同的重定位类型,我们只关心其中两种最基本的:
- R_X86_64_PC32:重定位一个使用32位PC相对地址的引用
- R_X86_64_32:重定位一个使用32位绝对地址的引用
重定位符号引用
使用不同的算法来重定位不同类型的引用
可执行目标文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xOa0VSZs-1626579806999)(C:\Users\吴鑫磊\AppData\Roaming\Typora\typora-user-images\image-20210718104055147.png)]
- ELF头描述文件的总体格式,还包括程序入口点(entry point),就是程序运行时要执行的第一条指令的地址
- .init节定义了一个小函数_init,程序的初始化代码会调用它
ELF文件被设计的很容易加载到内存,程序头部表描述了可执行文件的连续的片(chunk)被映射到连续的内存段的映射关系
加载可执行目标文件
-
通过shell命令运行可执行目标文件
-
LInux程序都可以通过调用execve函数调用加载器(loader)
-
加载器将程序复制到内存并运行,这个过程叫做加载
-
每个Linux程序都有一个运行时内存映像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihdwxEGX-1626579807000)(C:\Users\吴鑫磊\AppData\Roaming\Typora\typora-user-images\image-20210718105208253.png)]
动态链接共享库
使用静态库时,对于那些机会每个程序都会用到的标准I/O函数,他们总是会被复制到内存上,这是极大的浪费,而且静态库需要定期维护和更新。共享库(shared library)是一种致力于解决静态库缺陷的产物,在Linux系统通常使用**.so**后缀
共享库是一个目标模块,在运行或加载时可以加载到任意地址,并且和一个在内存中的程序链接起来,这个过程称为动态链接(dynamic linking),由动态链接器完成。
基本的思路是:在创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程
共享而非复制:没有任何共享库内容被复制到可执行文件中,而是链接器复制了一些重定位和符号表信息来在运行时解析共享库中代码和数据的引用
动态链接器需要重定位共享库中的数据和文本到某个内存段,重定位可执行目标文件中对共享库中定义的符号的引用,确定后共享库的位置在程序执行过程中都不会改变。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GBMHwgKw-1626579807003)(C:\Users\吴鑫磊\AppData\Roaming\Typora\typora-user-images\image-20210718111017818.png)]
从应用程序中加载和链接共享库
应用程序还可以在运行时要求动态链接器加载和链接某个共享库,Linux提供了简单的接口
位置无关代码
共享库的一个主要目的就是允许多个正在运行的进程共享内存中相同的库代码
现代系统以这样一种方式编译共享模块的代码:可以把他们加载到内存的任何位置而无需链接器修改
可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code)PIC
PIC数据引用
一个事实:代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的
生成PIC的编译器在数据段开始的地方创建了一个全局偏移量表(Global Offset Table,GOT),每个在本模块引用的全局数据目标都有一个条目,编译器还为每个条目生成一个重定位记录,加载时,动态链接器会重定位每个条目
PIC函数调用
延迟绑定(lazy binding):将过程地址的绑定推迟到第一次调用该过程时
过程绑定根据GOT和过程链接表(Procedure Linkage Table,PLT)这两个数据结构实现,GOT是数据段的一部分,PLT是代码段的一部分
如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的GOT和PLT
库打桩机制
库打桩:它允许你截获对共享库函数的调用,取而代之执行自己的代码
小结
链接器的两个主要任务就是符号解析和重定位
- 符号解析将每个全局符号都绑定在一个唯一的定义上(局部符号的绑定由编译器完成了)
- 确定每个符号的最终内存地址,并且修改对符号的引用
在将可重定位目标文件与静态库链接完之后
- 或加载时由加载器调用动态链接器进行动态链接
- 或运行中由某些接口加载和链接共享库