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
2
3
4
5
6
7
8
9
// test.c
void func();
void foo() {
func();
}

static int bss;
int arr[1024];
int dx[] = {0, 1, 2, 3};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
$ gcc -c -static test.c -o elf.out
$ readelf -a elf.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
前 16 字节描述的生成该文件的系统信息
Class: ELF64
ELF 类别
Data: 2's complement, little endian
补码, 小端法(第二章)
Version: 1 (current)
OS/ABI: UNIX - System V
系统类型和 ABI
ABI Version: 0
Type: REL (Relocatable file)
可重定位目标文件
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 664 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
描述各个 section 的起始/大小信息

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000011 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001f8
0000000000000018 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000060
0000000000000010 0000000000000000 WA 0 0 16
[ 4] .bss NOBITS 0000000000000000 00000080
0000000000001004 0000000000000000 WA 0 0 32
[ 5] [ 6] [ 7] [ 8] [ 9] 删去
[10] .symtab SYMTAB 0000000000000000 00000118
00000000000000c0 0000000000000018 11 4 8
[11] .strtab STRTAB 0000000000000000 000001d8
000000000000001c 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000228
000000000000006c 0000000000000000 0 0 1

Relocation section '.rela.text' at offset 0x1f8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000500000004 R_X86_64_PLT32 0000000000000000 func - 4

Relocation section '.rela.eh_frame' at offset 0x210 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
No processor specific unwind information to decode
重定向条目

Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000001000 4 OBJECT LOCAL DEFAULT 4 bss
4: 0000000000000000 17 FUNC GLOBAL DEFAULT 1 foo
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func
6: 0000000000000000 4096 OBJECT GLOBAL DEFAULT 4 arr
7: 0000000000000000 16 OBJECT GLOBAL DEFAULT 3 dx
这里的 UND, ABS 下面会说到
Properties: x86 ISA used:
x86 feature used: x86
$ readelf -x .text elf.out
Hex dump of section '.text':
0x00000000 554889e5 b8000000 00e80000 0000905d UH.............]
0x00000010 c3

翻译:
0x00000000: 55 push rbp
0x00000001: 48 89 e5 mov rbp, rsp
0x00000004: b8 00 00 00 00 mov eax, 0
0x00000009: e8 00 00 00 00 call 0x0000000e (这里通常是一个占位符,编译后会被替换)
0x0000000e: 90 nop
0x0000000f: 5d pop rbp
0x00000010: c3 ret

$ readelf -x .data elf.out
Hex dump of section '.data':
0x00000000 00000000 01000000 02000000 03000000 ...............

不是傻子都能看明白。

UNDEF, COMMON, ABS 是三个仅存在于可重定位文件中的伪节, 分别表示 当前还未定义的外部符号, 未初始化的全局变量不应该被重定位的符号

*bss: 鉴于新版 GCC 已经自动开启 -fno-common, 这里不再详细说明 COMMON

理解了各个节之后, 还需要理解如下定义:

  • 全局符号: 在某个模块中定义的非静态全局变量或函数, 可以被其他模块引用

  • 外部符号: 引用的其他模块符号的符号引用

  • 局部符号: 静态全局变量或静态函数, 不能被其他模块引用

考虑如何区分相同名字的全局符号:

  • 强符号: 函数 或 已初始化的全局变量
  • 弱符号: 未初始化的全局变量或

规则是: 有强符号优先解析为强符号, 多个强符号同名报错, 多个弱符号任选一个

这个规则会导致一些麻烦的事情

1
2
3
4
5
6
7
8
9
10
11
12
// foo.c
void f(void);

int y = 15212;
int x = 15213;

int main() { f(); }

// bar.c
double x;
void f() { x = -0.0; }

此时 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))

  1. 符号引用的重定位 (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

    因为 callq0x1, 而通常 x86_64 一条指令五个字节(例如 AT&T movl $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 的地址 + 1

    1
    2
    3
    4
    5
    6
    0x9 = 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 下 .text0x400000 开始.

动态链接:

动态链接是运行时将内存段中的符号定义重定位到内存段中的符号引用中, .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, 具体流程如下(配合书上的图阅读):

  1. 代码中 call func@PLT, 压入下一条地址作为返回地址并且跳转到对应的 PLT 条目,
  2. 而这个 PLT 条目会先跳到符号的 GOT 条目,
  3. 而 GOT 条目初始指向其 PLT 条目的下一条(即顺序执行, 之所以这么来回跳是为了延迟绑定, 绑定后就直接跳到 GOT 对应的实际地址而非继续顺序执行了)
  4. 而这个”下一条”就是 push 0x1; jmp *PLT[0];, 0x1addvec 的 ID
  5. 而 PLT[0] 干的是: 传入必要的重定位表地址作为参数, 跳转到动态链接器
  6. 动态链接器会处理重定位然后绑定对应的 GOT 条目, 然后跳转到 func
  7. func 结束后会从之前的 call 压入的返回地址返回

库打桩:

  • 编译期打桩: -I. 先找当前目录头文件, 在头文件里打包库函数
  • 链接时打桩: 利用静态符号解析: -Wl,--wrap,malloc 把 malloc 解析为 __wrap_malloc
  • 运行时打桩: 利用动态链接符号解析: LD_PRELOAD="./xxx.so" ./prog