背景
对于动态链接的详细工作过程一直很好奇,终于搞明白了其中的缘由,记录一下。
以前一直以为自己明白了,其实不明白。动手验证,细节就是魔鬼。
本次验证使用了 gdb,过程非常有趣,你也可以自己动手试试看。
测试代码
这是一个最小化的代码,调用的 printf 函数是 libc 提供的,因此就涉及到动态链接的过程了。
#include<stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
将源代码编译成可执行文件
1
gcc -g t.c
使用 gdb 分析动态链接过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ /usr/bin/gdb ./a.out
(gdb) start
(gdb) list
1 #include<stdio.h>
2
3 int main()
4 {
5 printf("hello world!\n");
6 return 0;
7 }
(gdb) disassemble
Dump of assembler code for function main:
0x0000000000401126 <+0>: push %rbp
0x0000000000401127 <+1>: mov %rsp,%rbp
=> 0x000000000040112a <+4>: mov $0x402010,%edi
0x000000000040112f <+9>: callq 0x401030 <puts@plt>
0x0000000000401134 <+14>: mov $0x0,%eax
0x0000000000401139 <+19>: pop %rbp
0x000000000040113a <+20>: retq
End of assembler dump.
(gdb)
我们看到,反汇编代码重并没有 printf 的调用,取而代之的是 puts 函数的调用。 这是因为 printf 的调用只有常量字符串没有参数,因此被 gcc 优化成 puts 的调用。
=> 0x000000000040112a <+4>: mov $0x402010,%edi 这条指令其实就是将 “hello world!\n” 的地址放在%edi。我们可以通过 x 这个 gdb 命令来确认。
(gdb) x /s 0x402010
0x402010: "hello world!"
通过该汇编语句 0x000000000040112f <+9>: callq 0x401030 <puts@plt>
我们知道
调用了 0x401030 这个位置的 plt 过程链接的函数。我们查看一下该地址的反汇编代码。
(gdb) disassemble 0x401030
Dump of assembler code for function puts@plt:
0x0000000000401030 <+0>: jmpq *0x2fca(%rip) # 0x404000 <puts@got.plt>
0x0000000000401036 <+6>: pushq $0x0
0x000000000040103b <+11>: jmpq 0x401020
End of assembler dump.
可以看到这个反汇编很短,只有 3 个指令。第一个指令的 jmpq 到另一个地址 0x404000 存储的指令地址去执行。
0x0000000000401030 <+0>: jmpq *0x2fca(%rip)
这个指令的 * 对理解 plt 的原理很重要。这里并不是直接
跳转到 0x2fca(%rip)
这个地址,而是跳转到 0x2fca(%rip)
这个地址存储的指令地址去执行,这里相当于是 C 语言的双重指针。
第二条指令是将一个常数参数压栈,第三个指令跳转到另一个地址 0x401020 去执行。
0x404000 这个地址是如何得到的呢?这个是通过 0x2fca(%rip) 计算得到的,而 %rip 的值是下一条指令的地址,
也就是 0x401036。所以 0x2fca + 0x401036 = 0x404000。我们来看看 0x404000 这个地址存储的是什么值。
因为 0x404000 存储的是一个地址,因此我们使用 x /a
的方式来查看该地址存储的值。
(gdb) x /a 0x404000
0x404000 <puts@got.plt>: 0x401036 <puts@plt+6>
从上面的输出可以看到 0x404000 存储的指令地址是 0x401036 <puts@plt+6>
, 这个是上面的 plt 函数的下一条要执行的指令。
也就是说 jmpq *0x2fca(%rip)
这个指令是跳转到另一个地址上存储的指令地址去执行,而这个存储的地址就是下一条指令。
如果不是跳转指令,那么上一条指令执行结束就是执行下一条指令。这里为什么要多此一举,通过 jmp
指令跳转到下一条指令呢?
这就是 plt 动态链接的关键之处了。 0x2fca(%rip)
原来存储的是 plt 的下一条指令,第一次执行会解析 puts 的函数地址,
将解析得到的函数地址存储在 0x2fca(%rip)
这个位置,下一次执行的时候就直接跳转到 puts 函数了。
1
2
3
4
5
6
(gdb) disassemble 0x401030
Dump of assembler code for function puts@plt:
0x0000000000401030 <+0>: jmpq *0x2fca(%rip) # 0x404000 <puts@got.plt>
0x0000000000401036 <+6>: pushq $0x0
0x000000000040103b <+11>: jmpq 0x401020
End of assembler dump.
我们再回顾一下上面的 plt 函数。可以看到下一条指令把 常数 0 压栈,然后跳转到 0x401020 去执行。我们看看 0x401020 这个地方的指令。
因为查看的是指令,因此使用 x /i
这样的 gdb 命令,下面的 6 表示打印 6 条指令。
(gdb) x /6i 0x401020
0x401020: pushq 0x2fca(%rip) # 0x403ff0
0x401026: jmpq *0x2fcc(%rip) # 0x403ff8
0x40102c: nopl 0x0(%rax)
0x401030 <puts@plt>: jmpq *0x2fca(%rip) # 0x404000 <puts@got.plt>
0x401036 <puts@plt+6>: pushq $0x0
0x40103b <puts@plt+11>: jmpq 0x401020
(gdb)
通过上面的指令,我们看到 把 0x2fca(%rip) # 0x403ff0
这个值压栈了,然后跳转到
*0x2fcc(%rip) # 0x403ff8
所存储的指令去执行了。压栈的这两个参数分别是代表什么呢?
第一个代表的是 puts 这个动态链接函数的索引,第二个代表 link_map 的结构。具体的可以参考 https://ypl.coffee/dl-resolve/ 这篇文章。
我们接下来看看 0x403ff8 这个位置存储的是指令地址是什么。
1
2
(gdb) x /a 0x403ff8
0x403ff8: 0x7ffff7dd0b20 <_dl_runtime_resolve_xsave>
可以看到 0x403ff8 地址存储的是 _dl_runtime_resolve_xsave , 用来解析 puts 的函数。关于该函数的原理可以参考 https://ypl.coffee/dl-resolve/ 。 我们接下来用 gdb 来分析一下 _dl_runtime_resolve_xsave 是如何获取 puts 这个字符串的。因为上面的参数一个是 0,一个是 link_map,并没有 puts 这个字符串参数。
1
2
3
4
5
6
7
8
9
10
11
12
[ljl@rocky8 openresty-develop]$ readelf -r ./a.out
Relocation section '.rela.dyn' at offset 0x458 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000403fc8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000403fd0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000403fd8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000403fe0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
Relocation section '.rela.plt' at offset 0x4b8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000404000 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
我们使用 readelf -r ./a.out
这个命令查询所有的 plt 函数,可以看到就只有一个 puts。
puts 排在第一个,以 0 作为起始值的索引计算,puts 的索引值为 0。
我们可以通过 readelf 来查看各个 section 的加载地址。因为这里是 exe 并且没有编译成共享类型的,因此加载的地址是不变的。 比如 我们可以看到 .rela.plt 这个段的加载地址是 0x4004b8。这些段的信息也存储在 link_map 中。
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
[ljl@rocky8 openresty-develop]$ readelf -S --wide ./a.out
There are 35 section headers, starting at offset 0x63c0:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000000004002a8 0002a8 00001c 00 A 0 0 1
...
[ 5] .dynsym DYNSYM 0000000000400328 000328 000090 18 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003b8 0003b8 000073 00 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040042c 00042c 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400438 000438 000020 00 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400458 000458 000060 18 A 5 0 8
[10] .rela.plt RELA 00000000004004b8 0004b8 000018 18 AI 5 22 8
[11] .init PROGBITS 0000000000401000 001000 00001b 00 AX 0 0 4
[12] .plt PROGBITS 0000000000401020 001020 000020 10 AX 0 0 16
[13] .text PROGBITS 0000000000401040 001040 000175 00 AX 0 0 16
[14] .fini PROGBITS 00000000004011b8 0011b8 00000d 00 AX 0 0 4
[15] .rodata PROGBITS 0000000000402000 002000 00001d 00 A 0 0 8
...
[20] .dynamic DYNAMIC 0000000000403df8 002df8 0001d0 10 WA 6 0 8
[21] .got PROGBITS 0000000000403fc8 002fc8 000020 08 WA 0 0 8
[22] .got.plt PROGBITS 0000000000403fe8 002fe8 000020 08 WA 0 0 8
[23] .data PROGBITS 0000000000404008 003008 000004 00 WA 0 0 1
[24] .bss NOBITS 000000000040400c 00300c 000004 00 WA 0 0 1
...
[32] .symtab SYMTAB 0000000000000000 005548 000720 18 33 56 8
[33] .strtab STRTAB 0000000000000000 005c68 0005fc 00 0 0 1
[34] .shstrtab STRTAB 0000000000000000 006264 000159 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),
D (mbind), l (large), p (processor specific)
使用 readelf 得到的加载地址来分析获取 puts 符号的过程
使用下面两个宏得到 symbol 的索引和符号的类型。
gdb 查看 符号。因为 .rela.plt 这个段的加载地址是 0x4004b8,而 puts 这个函数的 plt 索引是 0。 因此我们使用下面命令得到 put 函数的 Elf64_Rela 结构体。然后我们使用下面这两个宏对 r_info 字段 做运算,得到 puts 在 .dynsym 的索引是 2。然后我们打印 Elf64_Sym 结构体,通过 st_name 字段知道 puts 函数在字符串 .dynstr 段的偏移量是 1。
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
(gdb) print *(Elf64_Rela *) 0x4004b8
$5 = {
r_offset = 4210688,
r_info = 8589934599, # = 0x2,00000007
r_addend = 0
}
(gdb) print ((Elf64_Sym *)0x400328)[2]
$7 = {
st_name = 1,
st_info = 18 '\022',
st_other = 0 '\000',
st_shndx = 0,
st_value = 0,
st_size = 0
}
(gdb) x /s (0x4003b8 + 1)
0x4003b9: "puts"
通过 link_map 来解析 puts 这个符号
上面说过在调用 _dl_runtime_resolve_xsave 前向堆栈压入两个参数。第一个参数是 0,第二个参数是 0x403ff0。
我们通过 info symbol 0x403ff0
可以知道该地址是 got.plt 的第二个表项,这个表项存储的是 struct link_map *
的指针。
因此我们可以通过 x /a 0x403ff0
获取 struct link_map *
的指针值。
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
(gdb) info symbol 0x403ff0
_GLOBAL_OFFSET_TABLE_ + 8 in section .got.plt of /home/ljl/a.out
(gdb) x /a 0x403ff0
0x403ff0: 0x7ffff7ffe1f0
`
解下来我们通过 link_map 来获取 puts 这个符号。
```shell
(gdb) set print pretty on
(gdb) set pagination off
(gdb) print *(struct link_map *)0x7ffff7ffe1f0
$8 = {
l_addr = 0,
l_name = 0x7ffff7ffe790 "",
l_ld = 0x403df8,
l_next = 0x7ffff7ffe7a0,
l_prev = 0x0,
l_real = 0x7ffff7ffe1f0,
l_ns = 0,
l_libname = 0x7ffff7ffe778,
l_info = {0x0, 0x403df8, 0x403ed8, 0x403ec8, 0x0, 0x403e78, 0x403e88, 0x403f08, 0x403f18, 0x403f28, 0x403e98, 0x403ea8, 0x403e08, 0x403e18, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x403ee8, 0x403eb8, 0x0, 0x403ef8, 0x0, 0x403e28, 0x403e48, 0x403e38, 0x403e58, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x403f48, 0x403f38, 0x0 <repeats 13 times>, 0x403f58, 0x0 <repeats 25 times>, 0x403e68},
l_phdr = 0x400040,
l_entry = 4198496,
...
l_audit = 0x7ffff7ffe670
}
...
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
...
#define DT_JMPREL 23 /* Address of PLT relocs */
获取 plt 段的值, 这里使用到 23 这个值,定义可以参考上面。 因为 puts 在 plt 段的索引值是 0,下面用到的 0 索引就是这个原因。
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
(gdb) print *((struct link_map *)0x7ffff7ffe1f0)->l_info[23]
$13 = {
d_tag = 23,
d_un = {
d_val = 4195576, # 0x4004F8
d_ptr = 4195576
}
}
(gdb) print ((Elf64_Rela *)4195576)[0]
$14 = {
r_offset = 4210688,
r_info = 8589934599,
r_addend = 0
}
// #define ELF64_R_SYM(i) ((i) >> 32) 高 32 bit 得到在 dynsym 表中的索引值
(gdb) print ((Elf64_Rela *)4195576)[0].r_info >> 32
$15 = 2
// 获取 dynsym 段的值 DT_SYMTAB 6
(gdb) print *((struct link_map *)0x7ffff7ffe1f0)->l_info[6]
$16 = {
d_tag = 6,
d_un = {
d_val = 4195112,
d_ptr = 4195112
}
}
// 获取索引值为 2 的表项
(gdb) print ((Elf64_Sym*)4195112)[2]
$17 = {
st_name = 6,
st_info = 18 '\022',
st_other = 0 '\000',
st_shndx = 0,
st_value = 0,
st_size = 0
}
// 获取 dynstr 段 DT_STRTAB 5
(gdb) print *((struct link_map *)0x7ffff7ffe1f0)->l_info[5]
$20 = {
d_tag = 5,
d_un = {
d_val = 4195304,
d_ptr = 4195304
}
}
(gdb) x /s (4195304 + 6)
0x4003ee: "puts"
上面的 gdb 命令我们一步步执行,通过 plt 的索引值 和 got.plt 的 link_map 地址得到了 puts
的值。
总结
上面我们通过两种方法来获取 puts 这个字符串的来源。 一个是 gdb 结合 readelf 的方法,一个是 gdb 结合 link_map 字段的方法。 两种方法我们最终都得到代理 puts 这个字符串。