Compile/Loader/Libraries: Part 2 Dynamic linking

在上一篇,我们了解了静态链接和对应的环境 https://blog.mwish.me/2024/11/20/Compile-Loader-Libraries-Part-1-Static-linking/ 这部分我们需要介绍加载、动态链接和库

可执行文件的 Load 和进程

作者(有一些咬文嚼字的)认为:

  • 程序是静态的概念,指编译好的指令/数据的文件(那个 ELF 可执行文件?),因为这个原因,所以有的时候可执行文件还会被称为映像文件(image)
  • 进程是动态的概念,指被加载、运行的程序进程,有自己的虚拟地址空间

又翻到一张 rCore 的地址空间图。书上还有很多32位地址空间的讨论,但这块没看的必要了。

img

在书上提到了程序 Binary 的内存管理的方式,Overlay 和 Paging。Paging 是我们今天很熟悉的方式,以内存映射的方式来读取,Overlay 是以树形结构或者别的结构,自己 aware 程序的结构,并手动管理子结构的换入换出

img

Paging 映射的方式本质上也是现在操作系统 mmap / 内存管理的方式。在进程建立后(一些流程可能会在之后详细描述),这里执行 fork + exec 的逻辑大概是:

  1. 创建一个独立的虚拟地址空间
    1. 设置 PageTable 映射之类的
  2. 读取可执行文件头,进行映射
    1. VMA 上构建磁盘上 section 到内存的映射
    2. ELF 上有几种 section: 可执行的 text,可读可写的(比如 .bss)和只读的 (.strtab 等多种)。对于类似权限的 section,链接的时候可以某种程度上合并到一起,而 Load 的时候,也可以把它们一起 Load 。这种对象可以被称之为 segment,如图 6-7。Segment 包含一个或者多个属性类似的 Section。readelf -S 能显示对应的 Segment,readelf -l 也能看到被 load 的方式
  3. 指令寄存器设置到对应地址,启动!

img

这里可以尝试下面的程序,进入 llvm 例子阶段!

1
2
3
4
5
6
7
8
#include<unistd.h>

int main() {
while (1) {
sleep(1000);
}
return 0;
}

然后 readelf 查看 section 和 segments

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
// llvm-readelf -S a.out 
There are 33 section headers, starting at offset 0xd4d60:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400238 000238 000020 00 A 0 0 4
[ 2] .rela.plt RELA 0000000000400258 000258 000108 18 AI 0 24 8
[ 3] .init PROGBITS 0000000000401000 001000 00001a 00 AX 0 0 4
[ 4] .plt PROGBITS 0000000000401020 001020 000058 00 AX 0 0 8
[ 5] .text PROGBITS 0000000000401080 001080 092c26 00 AX 0 0 16
[ 6] __libc_thread_freeres_fn PROGBITS 0000000000493cb0 093cb0 0000b2 00 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 0000000000493d70 093d70 001aef 00 AX 0 0 16
[ 8] .fini PROGBITS 0000000000495860 095860 000009 00 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000496000 096000 01949c 00 A 0 0 32
[10] .stapsdt.base PROGBITS 00000000004af49c 0af49c 000001 00 A 0 0 1
[11] __libc_thread_subfreeres PROGBITS 00000000004af4a0 0af4a0 000008 00 A 0 0 8
[12] __libc_subfreeres PROGBITS 00000000004af4a8 0af4a8 000050 00 A 0 0 8
[13] __libc_IO_vtables PROGBITS 00000000004af500 0af500 0006a8 00 A 0 0 32
[14] __libc_atexit PROGBITS 00000000004afba8 0afba8 000008 00 A 0 0 8
[15] .eh_frame_hdr PROGBITS 00000000004afbb0 0afbb0 0022b4 00 A 0 0 4
[16] .eh_frame PROGBITS 00000000004b1e68 0b1e68 00dd58 00 A 0 0 8
[17] .gcc_except_table PROGBITS 00000000004bfbc0 0bfbc0 000105 00 A 0 0 1
[18] .tdata PROGBITS 00000000004c0ec0 0bfec0 000020 00 WAT 0 0 16
[19] .tbss NOBITS 00000000004c0ee0 0bfee0 000038 00 WAT 0 0 16
[20] .init_array INIT_ARRAY 00000000004c0ee0 0bfee0 000010 08 WA 0 0 8
[21] .fini_array FINI_ARRAY 00000000004c0ef0 0bfef0 000010 08 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000004c0f00 0bff00 0000e4 00 WA 0 0 32
[23] .got PROGBITS 00000000004c0fe8 0bffe8 000008 00 WA 0 0 8
[24] .got.plt PROGBITS 00000000004c1000 0c0000 000070 08 WA 0 0 8
[25] .data PROGBITS 00000000004c1080 0c0080 001690 00 WA 0 0 32
[26] .bss NOBITS 00000000004c2720 0c1710 002158 00 WA 0 0 32
[27] __libc_freeres_ptrs NOBITS 00000000004c4878 0c1710 000030 00 WA 0 0 8
[28] .comment PROGBITS 0000000000000000 0c1710 000070 01 MS 0 0 1
[29] .note.stapsdt NOTE 0000000000000000 0c1780 000f40 00 0 0 4
[30] .symtab SYMTAB 0000000000000000 0c26c0 00b9e8 18 31 810 8
[31] .strtab STRTAB 0000000000000000 0ce0a8 006b47 00 0 0 1
[32] .shstrtab STRTAB 0000000000000000 0d4bef 000171 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), l (large), p (processor specific)

// llvm-readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x401c3d
There are 9 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000360 0x000360 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x094869 0x094869 R E 0x1000
LOAD 0x096000 0x0000000000496000 0x0000000000496000 0x029cc5 0x029cc5 R 0x1000
LOAD 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x001850 0x0039e8 RW 0x1000
NOTE 0x000238 0x0000000000400238 0x0000000000400238 0x000020 0x000020 R 0x4
TLS 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x000020 0x000058 R 0x10
GNU_EH_FRAME 0x0afbb0 0x00000000004afbb0 0x00000000004afbb0 0x0022b4 0x0022b4 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x000140 0x000140 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.ABI-tag .rela.plt
01 .init .plt .text __libc_thread_freeres_fn __libc_freeres_fn .fini
02 .rodata .stapsdt.base __libc_thread_subfreeres __libc_subfreeres __libc_IO_vtables __libc_atexit .eh_frame_hdr .eh_frame .gcc_except_table
03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
04 .note.ABI-tag
05 .tdata .tbss
06 .eh_frame_hdr
07
08 .tdata .init_array .fini_array .data.rel.ro .got
None .comment .note.stapsdt .symtab .strtab .shstrtab

这里对应映射如图(书有点老,意思一下就行):

  • Type 有 LOAD 等类型的 Segments,fileSiz 是文件上的大小,MemSiz 是映射的内存上的大小。如果 MemSiz 更大,后面需要初始化成 0
  • BSS 段之类的内容要被 zero-initialize,初始化成 0,对于 0x001850 0x0039e8 这种,可能后面可以给 bss 开辟空间,令其被 zero-initialized

img

img

这里运行上面的程序,来查看 /proc 看虚拟空间(这里毕竟是一个 sleep 程序):

  1. VMA 前四个是执行文件的 Segment
  2. 后面是 Anonymous Virtual Memory Area,没有影射到文件中
  3. Vdso 是一个内核模块,它和 vsyscall 可以参考下面的博客,大概意思是 vdso / vsyscall 都是一些 syscall 绕过陷入内核的机制。
    1. Linux vDSO概述 - 乾越的文章 -https://zhuanlan.zhihu.com/p/436454953
    2. https://stackoverflow.com/questions/19938324/what-are-vdso-and-vsyscall
  4. 相比书中写作的时候,如果你 -fPIE,这里还有合适的 ASLR 机制,能够让访问的地址并不固定。不过现在咱们静态编译也没这个问题,这段我们后面还能遇到
  5. 借助 OS 的机制,可执行文件可能会有物理上的段合并,如上图 6-11
1
2
3
4
5
6
7
8
9
10
cat /proc/{pid}/maps             
00400000-00401000 r--p 00000000 fd:11 9043977 learn-compile/a.out
00401000-00496000 r-xp 00001000 fd:11 9043977 learn-compile/a.out
00496000-004c0000 r--p 00096000 fd:11 9043977 learn-compile/a.out
004c0000-004c3000 rw-p 000bf000 fd:11 9043977 learn-compile/a.out
004c3000-004c5000 rw-p 00000000 00:00 0
02418000-0243b000 rw-p 00000000 00:00 0 [heap]
7ffffe973000-7ffffe995000 rw-p 00000000 00:00 0 [stack]
7ffffe9f8000-7ffffe9fa000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

我们现在知道 Huge Page 也有被用到程序中,用于优化一些程序加载,如下面的几个例子:

动态链接

静态链接的 Binary 都要链到一起,all in one binary 是个好主意(甚至现在很多地方都推荐源码整个编译)。解决方案是某种程度上分离程序,然后「动态」的

img

DLL/.so/.dylib 在这里也有版本问题,即 DLL Hell,早期的 so/dll 缺乏版本管理机制,导致这块很折磨人。

动态链接是把程序拆分开来,在运行的时候才链接成一个完整的程序。显然,这里执行的时候涉及多个文件,也需要操作系统的支持(如上图,你显然要合适的给进程分配出合适的位置)。ELF 动态链接文件被称为动态共享对象(DSO, Dynamic Shared Objects),一般以 .so 为扩展名,Windows 则常是 .dll,Mac 下则是 .dylib。Linux 下常用的 C 语言的运行库称之为 glibc,常放在 /lib 下的 libc.so。系统的动态连接器会负责加载它,然后绑定没有决议的符号,进行 Relocate。

下面给出测试和实验的 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// cat lib.c  
#include <stdio.h>

void foobar(int i) {
printf("printing from lib.so %d\n", i);
}
// cat lib.h
#pragma once

void foobar(int i);
// cat program1.c
#include "lib.h"

int main() {
foobar(1);
return 0;
}
// cat program2.c
#include "lib.h"

int main() {
foobar(2);
return 0;
}

这里编译一把 clang -fPIC -shared -o lib.so lib.c 后,可以再去编译:clang -o program1 program1.c lib.so,然后就能跑了(记得 LD_LIBRARY_PATHLD_PRELOAD )

img

这里我们看到,链接的时候,我们还是指定了 lib.dylib 作为参数. 在这里,我们首先知道:

  1. 如果编译 program1.cprogram1.o,这个 ELF 文件是不知道 foobar 调用是动态还是静态的,相关信息都躺在 rela 里头
  2. 链接的时候,需要接受 lib 作为输入文件,它里面也保存了完整的符号信息,链接器知道最终是一个对动态符号的引用

在虚地址空间中,cat /proc/{pid}/maps 之后,能发现对 lib.so 和别的地方的映射 ,另外还有别的共享对象 ld-2.18.so,就是动态链接器

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
cat /proc/26247/maps 
555fa03bc000-555fa03bd000 r--p 00000000 fd:11 9044026 ../learn-compile/program1
555fa03bd000-555fa03be000 r-xp 00001000 fd:11 9044026 ../learn-compile/program1
555fa03be000-555fa03bf000 r--p 00002000 fd:11 9044026 ../learn-compile/program1
555fa03bf000-555fa03c0000 r--p 00002000 fd:11 9044026 ../learn-compile/program1
555fa03c0000-555fa03c1000 rw-p 00003000 fd:11 9044026 ../learn-compile/program1
7f3174554000-7f31746fe000 r-xp 00000000 fd:01 675913 /usr/lib64/libc-2.18.so.bak
7f31746fe000-7f31748fe000 ---p 001aa000 fd:01 675913 /usr/lib64/libc-2.18.so.bak
7f31748fe000-7f3174902000 r--p 001aa000 fd:01 675913 /usr/lib64/libc-2.18.so.bak
7f3174902000-7f3174904000 rw-p 001ae000 fd:01 675913 /usr/lib64/libc-2.18.so.bak
7f3174904000-7f3174908000 rw-p 00000000 00:00 0
7f3174908000-7f3174929000 r-xp 00000000 fd:01 674975 /usr/lib64/ld-2.18.so
7f3174b10000-7f3174b13000 rw-p 00000000 00:00 0
7f3174b21000-7f3174b22000 rw-p 00000000 00:00 0
7f3174b22000-7f3174b23000 r--p 00000000 fd:11 9044017 ../learn-compile/lib.so
7f3174b23000-7f3174b24000 r-xp 00001000 fd:11 9044017 ../learn-compile/lib.so
7f3174b24000-7f3174b25000 r--p 00002000 fd:11 9044017 ../learn-compile/lib.so
7f3174b25000-7f3174b26000 r--p 00002000 fd:11 9044017 ../learn-compile/lib.so
7f3174b26000-7f3174b27000 rw-p 00003000 fd:11 9044017 ../learn-compile/lib.so
7f3174b27000-7f3174b28000 rw-p 00000000 00:00 0
7f3174b28000-7f3174b29000 r--p 00020000 fd:01 674975 /usr/lib64/ld-2.18.so
7f3174b29000-7f3174b2a000 rw-p 00021000 fd:01 674975 /usr/lib64/ld-2.18.so
7f3174b2a000-7f3174b2b000 rw-p 00000000 00:00 0
7fff460a6000-7fff460c8000 rw-p 00000000 00:00 0 [stack]
7fff460ea000-7fff460ec000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

还记得之前的装载属性吗?我们 llvm-readelf -l 看看呢:我们会发现这里的文件类型和普通类型不一样,同时,DYNAMIC 的属性出现了

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
llvm-readelf -l program1

Elf file type is DYN (Shared object file)
Entry point 0x1070
There are 11 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000268 0x000268 R 0x8
INTERP 0x0002a8 0x00000000000002a8 0x00000000000002a8 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x0005c0 0x0005c0 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x00020d 0x00020d R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000128 0x000128 R 0x1000
LOAD 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000264 0x000268 RW 0x1000
DYNAMIC 0x002df0 0x0000000000003df0 0x0000000000003df0 0x0001f0 0x0001f0 RW 0x8
NOTE 0x0002c4 0x00000000000002c4 0x00000000000002c4 0x000020 0x000020 R 0x4
GNU_EH_FRAME 0x002004 0x0000000000002004 0x0000000000002004 0x000034 0x000034 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000228 0x000228 R 0x1

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.ABI-tag
08 .eh_frame_hdr
09
10 .init_array .fini_array .data.rel.ro .dynamic .got
None .comment .symtab .strtab .shstrtab

地址无关代码和 GOT

最早期 PE/COFF 之类的地方引入了 “shared static library”,大概是需要一个固定分配的地址,见:https://maskray.me/blog/2023-12-03-linker-notes-on-pe-coff 。不管怎么样,感觉这种 “shared static library” 还是比较不方便的。

这里希望共享对象装载的时候能在任意地址装载,这里希望:链接的时候,绝对地址的引用不做重定位,推迟到 Load 的时候完成。之前静态链接则是「链接时重定位」,现在则变成了希望动态决定这些。

这个说法是比较简单的,但是程序编译的时候本身也有一个目标地址,如果 load 的时候再重定位不就行了吗!主意很好,但实际上这里麻烦可能在细一点的地方:之前静态链接的时候,大家都编译到一起,这没啥问题,但是反过来,动态链接那个被链接的共享对象,要在多个地方(进程)共享,可能不能比较方便的定下一个固定的地址

img

这里我们回到之前编译的参数:

  • 如果只使用 -shared,这里会在 Load 的时候重定位,直接在加载期间填写上一些对应的映射关系
  • 如果使用 -shared + -fPIC,就会生成位置无关代码(Position-independent Code),便于指令的共享。对于现代机器来说,这也不麻烦

这里可以写大概几种引用情况(希望生成书中的情况可以 -m32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int a;
extern int b;

extern void ext();

void bar() {
// inner module data access
a = 1;
// inter module data access
b = 2;
}

void foo() {
// inner module call
bar();
// inter module call
ext();
}

我们编译后 llvm-objdump -d 一下:

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
// llvm-objdump -d x.o 

x.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <bar>:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: c7 05 00 00 00 00 01 00 00 00 movl $0x1, (%rip) # 0xe <bar+0xe>
e: 48 8b 05 00 00 00 00 movq (%rip), %rax # 0x15 <bar+0x15>
15: c7 00 02 00 00 00 movl $0x2, (%rax)
1b: 5d popq %rbp
1c: c3 retq
1d: 0f 1f 00 nopl (%rax)

0000000000000020 <foo>:
20: 55 pushq %rbp
21: 48 89 e5 movq %rsp, %rbp
24: e8 00 00 00 00 callq 0x29 <foo+0x9>
29: b0 00 movb $0x0, %al
2b: e8 00 00 00 00 callq 0x30 <foo+0x10>
30: 5d popq %rbp
31: c3 retq

// llvm-objdump -r x.o

x.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000006 R_X86_64_PC32 .bss-0x8
0000000000000011 R_X86_64_REX_GOTPCRELX b-0x4
0000000000000025 R_X86_64_PLT32 bar-0x4
000000000000002c R_X86_64_PLT32 ext-0x4

RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
0000000000000040 R_X86_64_PC32 .text+0x20
  1. Foo 对 bar 调用是一条相对地址调用指令,忽略共享对象的全局符号表介入(Global Symbol Interposition)问题,这里可以当成相对地址的跳转;bar 对 a 的访问也通过相对地址
  2. 模块间的数据访问可能需要指向变量的数组,这里可以参考之前我们的 .got, 用 llvm-objdump -h {} --show-lma 可以找到。这里 GOT 也可以保留目标函数的地址。(我个人觉得,某种意义上,这里逻辑是,代码段不可改,所以抽象出了一个可以改的数据段 .got,同时处理这票逻辑)

我们看到上面,有 R_X86_64_PC32 ( 对 a 这个模块内 static 的访问,使用 PC 相对地址 ), R_X86_64_PLT32 (对 bar 和 ext 的访问) 和 R_X86_64_REX_GOTPCRELX ( 对 b 的访问) 。这里我们有个非常好的参考对象(相对原文),maskray 的博客是这里比较好的介绍(虽然这是他在介绍 relocation overflow 的时候的博客: https://maskray.me/blog/2023-05-14-relocation-overflow-and-code-models )。这里特殊的是 R_X86_64_REX_GOTPCRELX,逻辑类似 rdx = .got[n] = &var1 ,ELF 在数据段建立了一个 GOT 表

img

这里看上去很清晰了,但是特殊的情况是,一个模块引用了一个定义在共享对象的全局变量的时候,比如 static in global,然后这里可能会( 参考 https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam

  • 编译链接的时候,这里面创建 global 对象的一个副本
  • Got 如果发现这个数据,指向这个副本

这里还有个间接以来的行为,我们刚才说了 Unix 下动态链接定义导出符号的结果,那万一是下面这种有依赖的符号呢?实际上这里也需要被特别判定来处理。

1
2
static int a;
static int* p = &a;

这里 maskray 的 threadlocal 文章还提到了这块和 thread local 的行为:https://maskray.me/blog/2021-02-14-all-about-thread-local-storage

MaskRay 博客上的讨论

https://maskray.me/blog/2021-08-29-all-about-global-offset-table : 见 Compiler behavior 一节,这里定义三种符号

The address may be:

  • a link-time constant
  • the load base plus a link-time constant
  • dependent on runtime computation by ld.so -> 这是比较常见的会存储在 GOT 的场景

这里编译出来的 binary 会带上 Global offset Table(通常由 .got.got.plt 组成,.got.plt 持有 .got 需要的符号,而其余内容在 .got 中),他们持有 “link-time constant” 和动态链接产生的对象.

GOT 生成的重定位引用的第一个一个符号。当链接器第一次看到这样的引用符号时,它会在 GOT 中保留一个条目。对于引用相同符号的后续 GOT 生成重定位,链接器仅重用此条目。

这里还提到有的优化能把 .got.got.plt 组合到一起。

延迟绑定和 PLT

这里比较好的内容是博客:https://maskray.me/blog/2021-09-19-all-about-procedure-linkage-table

也可以参考:https://ctf-wiki.org/executable/elf/structure/dynamic-sections/

在我之前举的例子中,有一个 memcpy@plt

1
2
3
4
call    memcpy@PLT

// -fno-plt
call qword ptr [rip + memcpy@GOTPCREL]

为了直观的展示这部分的印象,可以贴出对应的图,这个图源来自 https://ctf-wiki.org/executable/elf/structure/dynamic-sections/

  1. 上面的图为第一次调用的情况
  2. 下面的图为随后调用的情况

img

这里可以看到一个 call memcpy@PLT 的记号。plt 是 ELF 中提供间接映射的东西。我们这里还有:

1
2
3
4
5
6
7
8
9
10
11
Disassembly of section .plt:

0000000000001020 <.plt>:
1020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip) # 0x4008 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: ff 25 e4 2f 00 00 jmpq *0x2fe4(%rip) # 0x4010 <_GLOBAL_OFFSET_TABLE_+0x10>
102c: 0f 1f 40 00 nopl (%rax)

0000000000001030 <foobar@plt>:
1030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 0x4018 <_GLOBAL_OFFSET_TABLE_+0x18>
1036: 68 00 00 00 00 pushq $0x0
103b: e9 e0 ff ff ff jmp 0x1020 <.plt>

.pltfoobar@plt 是一个对应的 stub,值得一提的事这里没有实际代码。前面的 _GLOBAL_OFFSET_TABLE_ 是一些预留的位置,可以看 maskray 的博客

In the assembly dump, foo@plt is such a stub. Note that foo@plt is not a symbol table entry. It is just that objdump displays the PLT entry as foo@plt. We use the notation to describe a stub. The stub consists of 3 instructions. The first jmpq instruction loads an entry from .got.plt and performs an indirect jump. The remaining two instructions will be described when introducing lazy binding.

.got.plt holds an array of word size entries. On some architectures (x86-32, x86-64) .got.plt[0] is the link time address of _DYNAMIC. .got.plt[1] and .got.plt[2] are reserved by ld.so. .got.plt[1] is a descriptor of the current component while .got.plt[2] is the address of the PLT resolver.

The subsequent entries are for resolved function addresses. Each entry is relocated by an R_*_JUMP_SLOT dynamic relocation describing the target function symbol. Resolving a function address is also called a binding. There are two binding schemes.

这里 _GLOBAL_OFFSET_TABLE_ 对应 _DYNAMIC 段 ( .dynamic ),在 got 的博客也有介绍。这里 .got.pltelf_machine_fixup_plt 来调用 plt resolver。

img

动态链接的过程

首先,在链接的输出程序中,会有一个 .interp 段。这里可以看到对应有一个 request program interpreter,这里配置链接器也可以参考:https://clang.llvm.org/docs/Toolchain.html#linker ,配置 -fuse-ld

1
2
3
4
5
llvm-readelf --all program1 | grep inter
[ 1] .interp PROGBITS 00000000000002a8 0002a8 00001c 00 A 0 0 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
01 .interp
02 .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt

ELF 中最重要的应该是 .dynamic 段,它有点像动态链接对应的 ELF Header。

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
learn-compile llvm-readelf -d lib.so
Dynamic section at offset 0x2e20 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x115c
0x0000000000000019 (INIT_ARRAY) 0x3e08
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3e10
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x200
0x0000000000000005 (STRTAB) 0x320
0x0000000000000006 (SYMTAB) 0x230
0x000000000000000a (STRSZ) 139 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x4000
0x0000000000000002 (PLTRELSZ) 96 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x488
0x0000000000000007 (RELA) 0x3e0
0x0000000000000008 (RELASZ) 168 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x3c0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x3ac
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
➜ learn-compile llvm-readelf -d program1
Dynamic section at offset 0x2df0 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [lib.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1204
0x0000000000000019 (INIT_ARRAY) 0x3dd8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3de0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x2e8
0x0000000000000005 (STRTAB) 0x3e8
0x0000000000000006 (SYMTAB) 0x310
0x000000000000000a (STRSZ) 151 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x4000
0x0000000000000002 (PLTRELSZ) 96 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x560
0x0000000000000007 (RELA) 0x4b8
0x0000000000000008 (RELASZ) 168 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffb (FLAGS_1) PIE
0x000000006ffffffe (VERNEED) 0x498
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x480
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0

这里也可以看到依赖的库

1
2
3
4
5
// ldd program1
linux-vdso.so.1 (0x00007fffb2953000)
lib.so => {}/learn-compile/lib.so (0x00007f356767f000)
libc.so.6 => /lib64/libc.so.6 (0x00007f35670b6000)
/lib64/ld-linux-x86-64.so.2 (0x00007f356746a000)

这里我们在看 lib.so, -s 打印符号表还会看到 .dynsym,这是动态链接用的符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
llvm-readelf -s lib.so  

Symbol table '.dynsym' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5
7: 0000000000001000 0 FUNC GLOBAL DEFAULT 8 _init
8: 000000000000115c 0 FUNC GLOBAL DEFAULT 11 _fini
9: 0000000000001130 44 FUNC GLOBAL DEFAULT 10 foobar

Symbol table '.symtab' contains 27 entries:
Num: Value Size Type Bind Vis Ndx Name
// ...

我们之前也介绍过 rela.text 之类的,实际上动态链接也有 .rela.dyn .rela.plt, 用来做类似的事情。

显式动态链接

在Linux 中,从文件本身的格式上来看,动态库实际上跟一般的共享对象没有区别,正如我们前面讨论过的。主要的区别是共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态连接器自动完成,对于程序本身是透明的;而动态库的装载则是通过一系列由动态链接器提供的 API,具体地讲共有 4 个:打开动态库 (dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库 (diclose ),程序可以通过这几个API对动态库进行操作。

TUM Codegen Paper 也提到了这个。最简单的 codegen 方式也就是 xjb 编译然后 dlopen 之类的去链接。

References