探究动态库函数的地址是在何时确定的

问题描述


写了一段main函数:

#include 
#include 
int main(int argc, char **argv)
{
	printf("hello world\n");
	printf("12345678\n");

	_exit(0);
}

很简单,两条打印。但是printf属于glibc动态链接库,是在程序运行的时候再加载链接进来的,那么这里的main函数是如何确定printf的地址的呢?

从main函数的反汇编开始分析

先看下main函数的反汇编:

0000000000400566 
: 400566: 55 push %rbp 400567: 48 89 e5 mov %rsp,%rbp 40056a: 48 83 ec 10 sub $0x10,%rsp 40056e: 89 7d fc mov %edi,-0x4(%rbp) 400571: 48 89 75 f0 mov %rsi,-0x10(%rbp) 400575: bf 24 06 40 00 mov $0x400624,%edi 40057a: e8 c1 fe ff ff callq 400440 40057f: bf 30 06 40 00 mov $0x400630,%edi 400584: e8 b7 fe ff ff callq 400440 400589: bf 00 00 00 00 mov $0x0,%edi 40058e: e8 9d fe ff ff callq 400430 <_exit@plt> 400593: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40059a: 00 00 00 40059d: 0f 1f 00 nopl (%rax)

很明显“callq 400440 puts@plt”表示要跳转执行printf函数,于是我们拉出来相关的反汇编:

0000000000400440 :
  400440:	ff 25 da 0b 20 00    	jmpq   *0x200bda(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400446:	68 01 00 00 00       	pushq  $0x1
  40044b:	e9 d0 ff ff ff       	jmpq   400420 <_init+0x20>

首先跳转到_GLOBAL_OFFSET_TABLE_+0x20存放的地址,而此处存放的地址是0x400446,我们用gdb确认下:

(gdb) b *0x40057a
(gdb) run
Starting program: /home/liuht/workspace/elf/a.out 

Breakpoint 1, 0x000000000040057a in main (argc=1, argv=0x7fffffffe488) at hello.c:5
5		printf("hello world\n");
(gdb) si
0x0000000000400440 in puts@plt ()
(gdb) p/x *0x601020 
$1 = 0x400446

确实如此,但是这里并没有调用printf,而是继续做了一次跳转,跳转到0x400420 地址,我们拉出相关的反汇编:

0000000000400420 <_exit@plt-0x10>:
  400420:	ff 35 e2 0b 20 00    	pushq  0x200be2(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400426:	ff 25 e4 0b 20 00    	jmpq   *0x200be4(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40042c:	0f 1f 40 00          	nopl   0x0(%rax)

这里继续做了一次跳转,跳转到了601010 <GLOBAL_OFFSET_TABLE+0x10>存放的地址,我们用gdb看下:

(gdb) p/x *(unsigned long long *)0x601010 
$3 = 0x7ffff7deee40

很明显,这不是我们当前程序的地址,而应该是一块动态载入的代码分配的地址,这难道就是printf?我们跳进去看下:

(gdb) p/x *(unsigned long long *)0x601010 
$3 = 0x7ffff7deee40
(gdb) si
_dl_runtime_resolve_xsave () at ../sysdeps/x86_64/dl-trampoline.h:71
71	../sysdeps/x86_64/dl-trampoline.h: No such file or directory.

很明显,这不是printf,而函数名可以看出来,这是动态链接器里面的函数。动态链接器用于动态链接,所以printf的地址应该是通过_dl_runtime_resolve_xsave确定的,也就是说_dl_runtime_resolve_xsave执行完了以后,printf的地址应该才能确定。那么我们如何知道printf的地址呢?这个其实也很简单,我们直接跟踪第二次printf执行应该就可以了:

(gdb) b *0x400584
Breakpoint 4 at 0x400584: file hello.c, line 6.
(gdb) c
Continuing.
hello world

Breakpoint 4, 0x0000000000400584 in main (argc=1, argv=0x7fffffffe488) at hello.c:6
6		printf("12345678\n");
(gdb) si
0x0000000000400440 in puts@plt ()
(gdb) si
_IO_puts (str=0x400630 "12345678") at ioputs.c:33
33	ioputs.c: No such file or directory.

printf的地址放在了<GLOBAL_OFFSET_TABLE+0x20>这里,GDB确认下:

(gdb) p/x *(unsigned long long *)0x601020
$8 = 0x7ffff7a7c6a0

果然这个位置的内容被改了,而且可以确定是被动态链接器改了。

结论一

Disassembly of section .got.plt:

0000000000601000 <_GLOBAL_OFFSET_TABLE_>:
  601000:	28 0e                	sub    %cl,(%rsi)
  601002:	60                   	(bad)  
	...
  601017:	00 36                	add    %dh,(%rsi)
  601019:	04 40                	add    $0x40,%al
  60101b:	00 00                	add    %al,(%rax)
  60101d:	00 00                	add    %al,(%rax)
  60101f:	00 46 04             	add    %al,0x4(%rsi)
  601022:	40 00 00             	add    %al,(%rax)
  601025:	00 00                	add    %al,(%rax)
  601027:	00 56 04             	add    %dl,0x4(%rsi)
  60102a:	40 00 00             	add    %al,(%rax)
  60102d:	00 00                	add    %al,(%rax)

上面的section存放动态函数的地址,但是在开始的时候并不是动态函数的实际地址,因为实际地址只有在链接器链接后才能确认,所以初始地址是跳转到动态链接器,动态链接器修改该地址,第二次执行时,就直接跳转到动态链接器修改后的地址。

最后一个疑问

这里还有一个疑问,动态链接器怎么知道要修改0x601020这个地址的内容呢?这里开始我也很不解,后来我查看了一下对应的elf section,发现了一个很可疑的section:

Relocation section '.rela.plt' at offset 0x3b8 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 _exit@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

这里指明了printf地址的位置,如果动态链接器知道printf对应该section的哪个entry,问题就解决了。为此我们再来看下:

0000000000400440 :
  400440:	ff 25 da 0b 20 00    	jmpq   *0x200bda(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400446:	68 01 00 00 00       	pushq  $0x1
  40044b:	e9 d0 ff ff ff       	jmpq   400420 <_init+0x20>

这里有个1的压栈,很可能对应.rela.plt’的第一个entry,如果是的话,那就完全对的上了。为此我们旁路验证下:

0000000000400430 <_exit@plt>:
  400430:	ff 25 e2 0b 20 00    	jmpq   *0x200be2(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400436:	68 00 00 00 00       	pushq  $0x0
  40043b:	e9 e0 ff ff ff       	jmpq   400420 <_init+0x20>

0000000000400440 :
  400440:	ff 25 da 0b 20 00    	jmpq   *0x200bda(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400446:	68 01 00 00 00       	pushq  $0x1
  40044b:	e9 d0 ff ff ff       	jmpq   400420 <_init+0x20>

0000000000400450 <__libc_start_main@plt>:
  400450:	ff 25 d2 0b 20 00    	jmpq   *0x200bd2(%rip)        # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
  400456:	68 02 00 00 00       	pushq  $0x2
  40045b:	e9 c0 ff ff ff       	jmpq   400420 <_init+0x20>

_exit和__libc_start_main分别压栈0和2,他们在.rela.plt中的entry也是0和2,那么基本上可以确定,动态链接器是通过知道printf在.rela.plt的哪个entry,从而得知printf的地址应该存放咋哪个位置。

你可能感兴趣的:(linux内核杂谈)