文章目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 本文目标
简要分析Linux动态链接过程中,函数的链接过程。
3. 动态链接过程分析
3.1 分析背景
. arm32 cortex a7
. linux 4.14.111
. ubuntu 16.04 arm core
. gcc 4.9
. glibc-2.31
注意,本文假定读者具备一定的编译链接相关的前置知识。
3.2 流程分析
3.2.1 程序代码
#include <stdio.h>
int main(void)
{
int i;
scanf("%d", &i);
printf("Hello, World!\n");
return 0;
}
编译:
arm-linux-gcc -o test test.c
3.2.2 从程序执行开始说起
当在控制台输入:
./test
开始,bash会调用 fork()
创建一个新的子进程,然后在新进程中调用 execve()
加载test程序,我们的分析从 execve()
系统调用开始:
sys_execve(filename, argv, envp)
...
do_execveat_common(AT_FDCWD, filename, argv, envp, 0)
...
search_binary_handler(bprm)
fmt->load_binary(bprm) = load_elf_binary(bprm)
...
/* 加载ELF程序的phdr */
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
/* 找到ELF程序的解释器程序并打开 */
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
...
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
kernel_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, &pos);
...
interpreter = open_exec(elf_interpreter);
...
}
}
...
/* 解释器程序 ld.so 的入口 */
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter, &interp_map_addr,
load_bias, interp_elf_phdata);
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
/* 启动解释器程序 ld.so */
start_thread(regs, elf_entry, bprm->p)
3.2.3 程序的动态链接初始化
从上小节看到,内核启动了程序的解释器程序 ld.so
,下面我们就从 ld.so
入口开始分析。
_start
_dl_start()
/* 计算 链接地址 和 运行时地址 差值: 用于修正重定位 */
bootstrap_map.l_addr = elf_machine_load_address ();
/* 重排 .dynamic 中的 _DYNAMIC[] + _DYNAMIC[]重定位 */
elf_get_dynamic_info (&bootstrap_map, NULL);
...
bootstrap_map.l_relocated = 1;
...
entry = _dl_start_final (arg)
...
start_addr = _dl_sysdep_start (arg, &dl_main)
...
user_entry = (ElfW(Addr)) ENTRY_POINT;
...
(*dl_main) (phdr, phnum, &user_entry, GLRO(dl_auxv)) = dl_main()
main_map = _dl_new_object ((char *) "", "", lt_executable, NULL,
__RTLD_OPENEXEC, LM_ID_BASE);
...
main_map->l_entry = *user_entry;
...
elf_get_dynamic_info (main_map, NULL);
...
/* LAZY_BIND的情形,将对函数的调用初始化为 &_dl_runtime_resolve */
unsigned i = main_map->l_searchlist.r_nlist;
while (i-- > 0) {
...
_dl_relocate_object (l, l->l_scope, GLRO(dl_lazy) ? RTLD_LAZY : 0,
consider_profiling)
...
ELF_DYNAMIC_RELOCATE (l, lazy, consider_profiling, skip_ifunc)
/* 程序 .got 表 got[1],got[2] 修正(如我们示例程序 test 的 .got 表) */
if (l->l_info[DT_JMPREL] && lazy) {
/* 修正 got[1]: 动态链接管理对象 link_mmap 指针 */
got[1] = (Elf32_Addr) l;
/* 修正 got[2]: 设定为函数GOT表项修正接口
* _dl_runtime_resolve(),动态链接的函数在初次调
* 用时,均调用GOT[2](即_dl_runtime_resolve()),
* 然后_dl_runtime_resolve()将函数调用got表项修正
* 为正确地址。如示例程序 test 中的 scanf(),printf() 调用。
*/
got[2] = (Elf32_Addr) &_dl_runtime_resolve;
}
...
}
return user_entry;
...
return start_addr;
...
/*
* 接下来,进入程序的 main() 函数
*/
在这一小节,我们只需要重点关注,程序的 GOT 表项
(即.got
段)got[1]
和 got[2]
赋值(即对后面反汇编代码
中 .got 段
中地址 0x20634, 0x20638
两处的赋值,0x20634 对应 got[1]
, 0x20638 对应 got[2]
):
got[1]
设定为动态链接管理对象指针link_mmap
指针,got[2]
设定为函数动态链接(调用地址修正)接口_dl_runtime_resolve()
。
3.2.4 函数的动态链接过程
初次调用触发对函数的动态链接过程,我们需从汇编层次来观察这些细节。反汇编程序:
arm-linux-objdump -D test > test.S
3.2.4.1 对函数的调用
00010470 <main>:
...
// 调用 scanf()
1048c: ebffffa7 bl 10330 <__isoc99_scanf@plt>
...
// 调用 printf()
10498: ebffff9b bl 1030c <puts@plt>
...
3.2.4.2 从 PLT 调用表 跳转到 _dl_runtime_resolve()
函数调用 PLT(Prodedure Linkage Table) 表
:
Disassembly of section .plt:
// 所有函数初次调用的 PLT 表项
000102f8 <puts@plt-0x14>:
// 将函数返回地址压入堆栈 (返回 main() 函数)
102f8: e52de004 push {lr} ; (str lr, [sp, #-4]!)
// lr = [0x10308] = 0x00010328
102fc: e59fe004 ldr lr, [pc, #4] ; 10308 <_init+0x1c>
// lr = 0x10308 + 0x00010328 = 0x20630
10300: e08fe00e add lr, pc, lr
// pc = [0x20630 + 8] = [0x20638] = &_dl_runtime_resolve, lr = 0x20638
// 跳转到 _dl_runtime_resolve 执行,该函数修正对应的 GOT 表项为真正的函数地址,如 scanf() 等。
// (地址 0x20638 指向 GOT 表 _GLOBAL_OFFSET_TABLE_ 的第3个表项,即 .got[2],编译内容为 0,
// 但被 ld.so 设定为 &_dl_runtime_resolve,参看前面的程序启动流程)
10304: e5bef008 ldr pc, [lr, #8]!
10308: 00010328 andeq r0, r1, r8, lsr #6
// 函数 printf 的 PLT 表项
0001030c <puts@plt>:
1030c: e28fc600 add ip, pc, #0, 12
10310: e28cca10 add ip, ip, #16, 20 ; 0x10000
10314: e5bcf328 ldr pc, [ip, #808]! ; 0x328
...
// 函数 scanf 的 PLT 表项
00010330 <__isoc99_scanf@plt>:
// ip = 0x10338
10330: e28fc600 add ip, pc, #0, 12
// ip = 0x10338 + 0x10000 = 0x20338
10334: e28cca10 add ip, ip, #16, 20 ; 0x10000
// pc = [0x20338 + 0x310] = [0x20648] = 0x000102f8, ip = 0x20648
// 地址 0x20648 为 scanf() 函数 在 GOT 表 _GLOBAL_OFFSET_TABLE_ 中的 对应入口:
// . 第1次调用函数 scanf(),从地址 0x20644 取得的数据为 <puts@plt-0x14> 的地址 0x000102f8 ,
// 赋给 pc,即跳转到地址 0x000102f8 <puts@plt-0x14> 处执行;然后 <puts@plt-0x14> 的代码调
// 用程序启动时设定的 GOT 表项修正函数 _dl_runtime_resolve,通过 _dl_runtime_resolve 修正
// 函数 scanf() 的 GOT 表项(地址 0x20648 处)为 scanf() 的地址,然后跳转到函数 scanf() 执行。
// . 第2次及以后调用函数 scanf(),从地址 0x20644 取得的数据为 scanf() 函数的真正地址,赋给
// pc ,即跳转到 scanf() 函数执行。
10338: e5bcf310 ldr pc, [ip, #784]! ; 0x310
...
函数调用 GOT表(.got 段,_GLOBAL_OFFSET_TABLE_)
:
Disassembly of section .got:
00020630 <_GLOBAL_OFFSET_TABLE_>:
// got[0]: .dynamic 段地址 (_DYNAMIC[])
20630: 00020548 andeq r0, r2, r8, asr #10
// got[1]: 3.2.3 小节中填充为 link_map 指针
20634: 00000000
// got[2]: 3.2.3 小节中填充为函数 _dl_runtime_resolve() 地址
20638: 00000000
// printf() 函数 GOT 表项:编译链接时填充为 PLT 调用表 首地址
// 首次调用后 _dl_runtime_resolve() 修正此表项为 printf() 函数的正确地址
2063c: 000102f8 strdeq r0, [r1], -r8
20640: 000102f8 strdeq r0, [r1], -r8
20644: 000102f8 strdeq r0, [r1], -r8
// scanf()函数GOT表项:编译链接时填充为PLT首表项地址
// 首次调用后 _dl_runtime_resolve() 修正此表项为 scanf() 函数的正确地址
20648: 000102f8 strdeq r0, [r1], -r8
2064c: 000102f8 strdeq r0, [r1], -r8
20650: 00000000 andeq r0, r0, r0
第1次
调用函数,跳转到 _dl_runtime_resolve()
函数的流程总结:
1. main() 对 `scanf()` 的调用,跳转到 `00010330 <__isoc99_scanf@plt>` 处执行;
2. 程序执行到地址 `10338` 处,该处指令通过查询 `scanf()` 的 地址 `20648 处 GOT 表项,跳转到
`000102f8 <puts@plt-0x14>` 处执行;
3. 程序执行到地址 `10304` 处,程序跳转到 `ld.so` 设定的 GOT[2] 函数`_dl_runtime_resolve()`处执行;
`_dl_runtime_resolve()` 修正当前被调用函数 GOT 表项为正确地址(如修正地址`20648`处的`scanf()`
函数的GOT表项)。
而 第2次
及后续对函数的调用,会从 GOT 表项处取得正确的调用地址
,然后直接跳转到函数执行
,而不用再跳转到 000102f8
处执行。
// 第1次 scanf 函数调用
main()
__isoc99_scanf@plt
// 此时 scanf 的 GOT 表项指向 puts@plt-0x14 ,跳转到 puts@plt-0x14 处,
// 调用 _dl_runtime_resolve() 【动态链接】 scanf:
// a. 将 scanf 的 GOT 表项修正为 scanf() 函数的地址
// b. 然后跳转到 scanf 函数执行
puts@plt-0x14
_dl_runtime_resolve()
/*
* 函数的动态链接过程:
*/
// a. 将 scanf 函数的 GOT 表项修正为正确地址
...
// b. 然后跳转到 scanf 函数执行
scanf()
// 第2次及后续 scanf 函数调用
main()
__isoc99_scanf@plt
// 此时 scanf GOT 表项指向 scanf 函数
scanf()
我们可以从反汇编的结果观察到,GOT
表中(即.got
段)除前3个表项
外,其它表项的值都为 102f8
。这就意味着,动态链接函数的第1次调用
,都会跳转到地址 102f8
处执行,然后通过 _dl_runtime_resolve()
,将函数调用的GOT表项
修正为正确地址(如示例中对 scanf() 的 GOT表项 20648: 000102f8 strdeq r0, [r1], -r8
的修正)。当然,我们这里描述的都是LAZY_BIND
的情形。
3.2.4.3 修正函数调用GOT表项,并跳转到函数执行
_dl_runtime_resolve:
/*
* 从分析程序反汇编代码的 .plt 段可知此时:
* . 调用的最终返回地址压在栈顶 (stack[0])
* . ip(r12) 寄存器包含被调用函数的 got 表项地址
* . lr 寄存器指向 GOT[2]
*
* GOT[] 表的分布如下:
* GOT[0] : .dynamic 段地址 (_DYNAMIC[])
* GOT[1] : 程序动态链接管理对象地址 (link_map *)
* GOT[2] : 修正 GOT 表项的函数地址 (&_dl_runtime_resolve)
* GOT[3...N]: 函数调用 GOT 表项, 在函数第一次被调用时由 _dl_runtime_resolve()
* 修正为函数的正确地址.
*/
push {r0-r4}
...
ldr r0, [lr, #-4] @ 程序动态链接管理数据指针 (link_map *),即 GOT[1]
sub r1, ip, lr
sub r1, r1, #4
add r1, r1, r1
bl _dl_fixup @ 修正被调用函数对应的 GOT 表项
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); /* .dynsym */
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); /* .dynstr */
const PLTREL *const reloc /* ElfRel, .rel.plt, 被调用函数 .rel.plt 重定位表项地址 */
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset/*reloc_arg*/);
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; /* 重定位相关符号 */
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); /* 应用重定位的地址: .got 表函数调用表项地址 */
/* value = 修正后的函数调用地址 */
...
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
/* 修正函数调用 .got 表项: *rel_addr = value */
return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
mov ip, r0 @ ip = 返回被调用函数的地址
pop {r0-r4,lr} @ 恢复参数寄存器以及调用的返回地址
BX(ip) @ 跳转到被调用函数 scanf() 或 printf()
3.2.5 动态链接相关数据段
.dynsym: 动态链接符号段
.dynstr: 动态链接字符串表
.rel.dyn: 动态链接重定位段
.rel.plt: 动态链接仅 PLT 相关的重定位段
.plt: 调用PLT(Prodedure Linkage Table)表
.dynamic: 动态链接段(_DYNAMIC[])
.got: 动态链接函数地址表
4. 后记
本文远未深入到动态链接的所有细节,动态链接是一个庞大而复杂的过程,如果全部展开的话应该能成书了。本文仅起到可能的引导作用,有兴趣的读者可自行深入挖掘。
5. 参考资料
ELF V1.2.pdf