Linux函数动态链接简析

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值