Linking
Linking
预处理、编译、汇编
预处理:gcc -E 编译器处理预处理命令,包括对头文件的包含,宏定义的展开,条件编译的选择等
编译:gcc -S 进行词法分析、语法分析、语义分析,进行代码优化和存储分配,生成汇编语言程序
汇编:gcc -c 将汇编语言代码转化为机器语言代码,生成可重定位目标文件
可重定位目标文件
链接视图(ELF格式)
“节”是 ELF 文件中具有相同特征的最小可处理单位。
| 节名 | 内容 |
|---|---|
.text | 机器指令(CPU 执行的二进制代码)。 |
.rodata | 只读数据(如 printf 里的字符串字面量、const 常量)。 |
.data | 已初始化且不为 0 的全局变量和静态(static)变量。 |
.bss | 未初始化或初始值为 0 的全局变量和静态(static)变量(在文件中不占空间,只占一个大小标记)。 |
.symtab | 符号表(记录了程序里所有全局变量、函数的名字和它们的地址映射)。 |
.strtab | 字符串表(存的是各个符号名字的纯文本字符串)。 |
.rela.text | 代码重定位表(.text 中需要重定位的条目)。 |
.rela.data | 数据重定位表,告诉链接器,.data 里哪些全局数据指针需要修正 |
节头表 | 描述每个节的基本信息,包括在文件里的偏移量、大小、访问属性、对齐方式等。 |
ELF头 | 目标文件基础信息,介绍这个文件是 32位还是64位、魔数以及程序执行的入口地址。 |
执行视图
“段”是具有某种共同的性质的节的集合
- 程序头表:描述可执行目标文件中的段与虚拟地址空间的映射关系,每个表项记录一个段的映射等相关信息
- 数据段/可读可写段:.data节和.bss节
- 代码段/只读代码段:.text和.rodata等
相对于链接视图,多一个程序头表(段头表),用于说明可执行目标文件的段的组成等;并且多一个.init节,其中定义了一个_init函数,用于可执行目标文件开始执行时的初始化工作;此外少了两个用于重定位的信息节(即.rel.text节和.rel.data节);
链接
将多个可重定位的目标文件合成一个可执行文件。
符号解析
将程序中每一个符号引用与一个确定的符号定义建立起唯一的关联
链接器会扫描所有输入的模块和库文件,按照强弱符号的规则解决多重定义问题,确保每个被引用的符号都能找到唯一确定的存储空间
- 全局符号:本模块内部定义并被其他模块引用的符号
- 本地符号:本模块内部定义并仅能由本模块内引用的符号
- 外部符号:由其他模块定义并被本模块引用的符号
全局符号分强弱符号:
- 强符号:函数名和已初始化的全局变量
- 弱符号:未初始化的全局变量名
Note
外部符号和本地符号无强弱之分,若出现重复定义,在编译阶段就能检查到
多重定义符号处理规则
- 强符号不能多次定义
- 一个符号被说明为一次强符号和多次弱符号定义,以强符号定义为准
- 若有多个弱符号定义,则任选其中一个
符号解析的过程
集合E:将被合并组成可执行文件的目标文件集合
集合U:未解析的符号集合
集合D:目标文件中已定义的符号集合
E, U, D 初始为空
扫描所有输入文件:
若为目标文件:
加入到E,定义的符号加入D,出现但暂未定义的符号加入U
若为库文件:
将U中所有符号与库文件各目标模块符号进行匹配,对于每个匹配到的目标模块:
目标模块移入E中,符号由U移到D
若遇到往D中加入已存在的符号或结束时U非空,链接报错通常链接器先处理目标文件,再按顺序扫描库文件,以便按需提取库文件内容
重定位
在合并生成执行文件时,重新确定每条指令的地址、每个数据的地址、在指令中确定所引用符号对应的地址。
- 合并相同的节
- 确定合并后符号的最终地址
- 修改引用地址,填入真实地址
重定位类型:
- R_386_PC32:引用处采用的是相对寻址方式,即有效地址 = PC的内容+重定位后的32位地址(偏移) 重定位前,
- R_386_32:指明引用处采用绝对地址方式,即有效地址 = 重定位后的32位地址
重定位前,此符号的初始值说明该符号在最终可执行文件中的地址:
- R_386_PC32 中初始值通常是
-4(该符号位置相对当前PC的偏移,PC已在下一条指令) - R_386_32 存储的是该符号在当前节内的偏移量
位置无关代码(PIC)
实现动态链接,必须实现位置无关代码(Position-Independent Code)
共享库代码是一种PIC
PLT: Procedure Linkage Table
GOT: Global Offset Table
访问动态链接的模块的数据:
- 编译器在编译时,利用我们之前聊过的
%rip相对寻址(或者 32 位下的%ebx),算出当前指令到 GOT 表的相对距离(这个距离在链接后是固定不变的)。 - 机器指令先去 GOT 表里对应
g_val的那个格子读一下。 - 动态链接器(ld.so) 在程序启动或加载时,会把
g_val此时此刻在当前进程里的绝对地址写进这个 GOT 格子里。 - CPU 从 GOT 格子里拿到真正的绝对地址,完成操作。
调用动态链接的函数:
- 第一次调用
printf:- 进程跳到
printf@plt。 - PLT 里的第一条指令是
jmp *printf@got(顺着 GOT 表去找地址)。 - 好玩的事情发生了:此时GOT 表里还没填
printf的真正地址,里面填的是 “PLT里下一条指令的地址” - 于是,程序跳回了 PLT 的后续代码。后续代码会调用动态链接器,动态链接器在内存里找到
printf的真实地址,并把这个真地址写入printf@got,最后去执行printf。
- 进程跳到
- 第二次及以后调用
printf:- 进程再次跳到
printf@plt。 - 再次执行
jmp *printf@got。 - 此时,由于 GOT 格子里已经是上次动态链接器写好的绝对地址了,直接跳进
printf函数体
- 进程再次跳到