CSAPP3e第七章(链接)
链接(第七章 ALL)
这章内容很短, 就是书中和 15213 都有意隐去了很多细节, 自己去理解思考这些细节会很麻烦
另外 section 的翻译易引起歧义, 这里不作翻译
静态链接:
静态链接包括两个阶段, 符号解析和重定位:
符号解析
符号解析是要把每一个符号引用与符号定义联系起来
首先要理解可重定向文件的各个 section
名称 | 意义 | 举例 |
---|---|---|
ELF Header | 描述 ELF 格式文件的各个 Section 信息 | 开始 16 字节描述了生成该文件的系统的大小和字节顺序, 剩下的描述目标文件类型/机器类型等 |
.text |
代码段, 已编译程序的机器代码 | E8 78 56 34 12 (callq 0x12345678 ) |
.rodata |
只读数据 | printf("%d", a) 中的
"%d" , switch 的跳转表 |
.data |
已初始化的全局和静态变量 | |
.bss * |
未初始化的或初始化为 0 的全局和静态变量 | static int bss = 0; int arr[N]; signed main() {} |
.symtab |
符号表, 定义和引用的符号信息 | 任意符号 |
.rel.text |
代码段符号的重定位条目(见下文) | void func(); 中
R_X86_64_PC32 func |
.rel.data |
数据段符号的重定位条目(见下文) | int arr[N]; 中
R_X86_64_32 arr |
.debug |
-g , 原始 C 文件 |
N/A |
.line |
行号映射 | N/A |
.strtab |
.symtab 和
.debug 中的符号表, Section Header 中的 section 名字 |
N/A |
Section Header | 描述目标文件的 section | N/A |
这里拿具体的例子展示一下, 环境是 x86_64 Linux, 使用
readelf
读 ELF 文件, 删去了一些不重要的输出,
中文为注释。
1 | // test.c |
1 | $ gcc -c -static test.c -o elf.out |
UNDEF, COMMON, ABS 是三个仅存在于可重定位文件中的伪节, 分别表示 当前还未定义的外部符号, 未初始化的全局变量 和 不应该被重定位的符号
*bss: 鉴于新版 GCC 已经自动开启
-fno-common
, 这里不再详细说明 COMMON
理解了各个节之后, 还需要理解如下定义:
全局符号: 在某个模块中定义的非静态全局变量或函数, 可以被其他模块引用
外部符号: 引用的其他模块符号的符号引用
局部符号: 静态全局变量或静态函数, 不能被其他模块引用
考虑如何区分相同名字的全局符号:
- 强符号: 函数 或 已初始化的全局变量
- 弱符号: 未初始化的全局变量或
规则是: 有强符号优先解析为强符号, 多个强符号同名报错, 多个弱符号任选一个
这个规则会导致一些麻烦的事情
1 | // foo.c |
此时 x : int
是强符号, 调用 f
时将会使用
x : int
赋值为 -0.0
然而栈里是这样排布的 1
2| y : 0x601024, 四字节 |
| x : 0x601020, 四字节 |-0.0
是八字节的, 除了填充 x 的四字节还会填充 y
的四字节导致 y 变化
关于链接器解析符号的过程, E, U, D 集合的部分比较简单不再赘述, 直接翻书即可
由于新版 GCC 已经默认
fno-common
, 这段代码会报错
重定位
重定位的过程也分为两个部分: 1. section 和 符号定义 的重定位 (取得
ADDR(r.symbol)
)
符号引用的重定位 (
R_X86_64_PC32
, 或R_X86_64_32
两种方式)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 每个重定位条目如下:
typedef struct {
long offset; /* Offset of the reference to relocate */
long type: 32, /* Relocation type */
symbol: 32; /* Symbol table index */
long addend; /* 这个修正下面会说 */
} Elf64_Rela;
// 对于一个 section s 和 一个 重定位条目结构体 r
// 获取符号引用的指针
refptr = s + r.offset;
// 如果是绝对重定位 R_X86_64_32, 直接赋值
*refptr = ADDR(r.symbol) + addend; /* addend == 0 */
// 如果是相对重定位 R_X86_64_PC32, 需要补正, 下面会解释
*refptr = ADDR(r.symbol) + addend - (ADDR(s) + r.offset) /* addend == -4 */对于相对重定位的补正, 我们举个例子:
1
0x1: callq 0x9
注意在重定位前
0x9
的位置通常是个占位符, 这里是说明被调用的代码的位置是0x9
callq
机器码的位置是0x1
,refptr
指向0x2
的位置, 要修改为相对重定向后的地址先手动计算一下这个位置应当被替换为的值:
我们知道 call 指令是压 PC 入栈作为返回值 + 跳转到 PC + 参数 的位置, 设我们需要重定位替换的参数为 x
1
0x9 == PC + x
因为
callq
在0x1
, 而通常 x86_64 一条指令五个字节(例如 AT&Tmovl $1, %eax
机器码是B8 01 00 00 00
) 所以当前的 PC 应为0x1 + 0x5 = 0x6
得到
x = 0x3
.那么我们如何通过上面的代码实现这个
0x3
的计算呢? 实际上就是靠addend
的补正作用列出一些参数:
内容 值 ADDR(r.symbol)
0x9
addend
待计算 ADDR(s) + r.offset
0x2
之所以是
0x2
而不是0x1
是因为ADDR(s) + r.offset
是段起始地址+重定位条目位置相对的偏移, 也就是说实际上应该是callq
那个参数(当前为占位符) 的地址, 这也启发我们知道这个值其实就是callq
的地址 + 11
2
3
4
5
60x9 = PC + x
*refptr = x
= 0x9 - PC
= 0x9 - (ADDR(callq) + 5)
= 0x9 - (ADDR(callq) + 1 + 4)
= ADDR(r.symbol) - (ADDR(S) + r.offset) - 4于是得出
addend
实际上为 \(-4\)十六进制写 \(\LaTeX\) 太怪了, 就贴了个 plain text
可执行文件:
可执行文件无伪节, 无 .rel
section.
任何 Linux 可执行文件都是 execve
系统调用通过一段操作系统驻留在主存内的代码 loader
执行的
loader
会试图调用 ctrl.o
中的
_start
函数, 后者会继续调用 libc.so
中的
__libc_start_main
函数, 最后才会调用到用户的
main
函数.
Linux x86_64 ELF64 下 .text
从 0x400000
开始.
动态链接:
动态链接是运行时将内存段中的符号定义重定位到内存段中的符号引用中,
.interp
section记录了链接器的路径
可用 dlopen, dlsym, dlclose, dlerror
来在 C
代码中手动加载动态链接库(.so), 加载符号等
由于共享库在内存中的加载位置不确定(你总不能固定下来位置给某个库然后一些不需要的库也浪费着占位吧),
所以需要位置无关代码(Position Independent Code, PIC. GCC 中 使用
-fpic
调用
PIC 数据引用:
原理: 数据段与代码段之间的距离是位置无关的(固定的), 设为 delta.
在代码段引用数据段的数据的时候, 只需要在数据段开头的 GOT (Global Offset
Table) 里存下符号定义的位置(&sym
), 则 delta 就是 PC 和
GOT[i]
的差值. 编译器为 GOT 每一个 8
字节条目生成重定位条目, 代码里随便写符号引用, 编译为可重定位目标文件时
直接引用 PC + delta
(即 GOT[i]
)
此时我们还需要确保在 GOT 存的 &sym
也是位置无关的,
所以在加载时利用动态链接器重定向每个 GOT[i]
的实际符号定义的位置就行了
PIC 函数调用:
利用 GOT 和 PLT (Procedure Linkage Table, 在代码段) 的协作:
首先介绍一些约定俗成的 PLT 位置值:
PLT[0]
处的代码传参并跳转到动态链接器PLT[1]
跳__libc_start_main
剩下的条目用于处理需要调用的位置无关函数
PIC 的函数调用不会直接重定向到函数地址,
因为不知道动态链接库在内存中加载的具体位置 调用某个函数时,
我们调用这个函数对应的 PLT 条目 func@PLT
,
具体流程如下(配合书上的图阅读):
- 代码中 call func@PLT, 压入下一条地址作为返回地址并且跳转到对应的 PLT 条目,
- 而这个 PLT 条目会先跳到符号的 GOT 条目,
- 而 GOT 条目初始指向其 PLT 条目的下一条(即顺序执行, 之所以这么来回跳是为了延迟绑定, 绑定后就直接跳到 GOT 对应的实际地址而非继续顺序执行了)
- 而这个”下一条”就是
push 0x1; jmp *PLT[0];
,0x1
是addvec
的 ID - 而 PLT[0] 干的是: 传入必要的重定位表地址作为参数, 跳转到动态链接器
- 动态链接器会处理重定位然后绑定对应的 GOT 条目, 然后跳转到 func
- func 结束后会从之前的 call 压入的返回地址返回
库打桩:
- 编译期打桩:
-I.
先找当前目录头文件, 在头文件里打包库函数 - 链接时打桩: 利用静态符号解析:
-Wl,--wrap,malloc
把 malloc 解析为 __wrap_malloc - 运行时打桩: 利用动态链接符号解析:
LD_PRELOAD="./xxx.so" ./prog