就用这个图来解释。
其中红色的线是第一次调用库函数的时候程序执行流的路线。蓝色的是程序以后调用要走的路线。
0x01为什么要用这种方式
共享库的一个主要目的是允许多个正在运行的进程共享内存中相同的代码库。如果给这个库分配固定的位置是很简单,但是如果没有一个进程在使用这个库那这部分空间不是也要空出来,并且很难管理这些空间。为了解决这一问题设计了一种动态加载的库,这种库里面的代码跟位置没有关系。每次加载到内存中的位置也是随机的。
0x02
上图中出现了2个名词
PLT(过程链接表):PLT是一个数组,每个条目是16字节代码。PLT[0]比较特殊它跳转到动态连接器中,每个可执行程序调用的库函数都有自己的PLT,每个条目都负责调用一个具体的函数。PLT[1]调用系统启动函数(__lib_start_main)。 是代码段的一部分。
GOT(全局偏移量表):
是一个数组,每个条目是8个字节地址GOT[0]和GOT[1]包含动态链接器在解析函数地址时会用到的信息GOT[2]是动态连接器在ld-linux.so模块中的入口点。其余每个条目对应于被调用的函数。其地址需要在运行时被解析,每个条目都有一个与之对应的plt条目。
0x03过程
1:代码中调用外部函数func,语句形式为call 0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。
2:PLT表的形式如下
.PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1: jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2: jmp *func@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。
4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl offset,其中offset是符号func的重定位偏移量。注意到这是一个二次跳转。
5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目(.PLT0),把GOT[1]的内容(放置了用来标识特定库的代码)压栈,并跳转到GOT[2]对应的地址。
6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。
7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。
动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。
0x04举例
源代码:
#include <stdio.h>
void hello_world(void)
{
printf("Hello world!\n");
return;
}
int main(int argc, char* argv[])
{
hello_world();
return 0;
}
编译并反汇编:
gcc -g test.c -o test
objdump -S test
0000000000400537 <main>:
int main(int argc, char* argv[])
{
400537: 55 push %rbp
400538: 48 89 e5 mov %rsp,%rbp
40053b: 48 83 ec 10 sub $0x10,%rsp
40053f: 89 7d fc mov %edi,-0x4(%rbp)
400542: 48 89 75 f0 mov %rsi,-0x10(%rbp)
hello_world();
400546: e8 db ff ff ff callq 400526 <hello_world>
return 0;
40054b: b8 00 00 00 00 mov $0x0,%eax
}
需要注意的是
400546: e8 db ff ff ff callq 400526 <hello_world>
void hello_world(void)
{
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
printf("Hello world!\n");
40052a: bf e4 05 40 00 mov $0x4005e4,%edi
40052f: e8 cc fe ff ff callq 400400 <puts@plt>
return;
400534: 90 nop
}
400535: 5d pop %rbp
400536: c3 retq
这里的printf函数的调用
40052f: e8 cc fe ff ff callq 400400 <puts@plt>
看一下0x400400位置处的内容:
0000000000400400 <puts@plt>:
400400: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
400406: 68 00 00 00 00 pushq $0x0
40040b: e9 e0 ff ff ff jmpq 4003f0 <_init+0x28>
0x4003f0: push QWORD PTR [rip+0x200c12] # 0x601008
0x4003f6: jmp QWORD PTR [rip+0x200c14] # 0x601010
0x4003fc: nop DWORD PTR [rax+0x0]
=> 0x400400 <puts@plt>: jmp QWORD PTR [rip+0x200c12] # 0x601018
| 0x400406 <puts@plt+6>: push 0x0
| 0x40040b <puts@plt+11>: jmp 0x4003f0
| 0x400410 <__libc_start_main@plt>: jmp QWORD PTR [rip+0x200c0a] # 0x601020
| 0x400416 <__libc_start_main@plt+6>: push 0x1
|-> 0x400406 <puts@plt+6>: push 0x0
0x40040b <puts@plt+11>: jmp 0x4003f0
0x400410 <__libc_start_main@plt>: jmp QWORD PTR [rip+0x200c0a] # 0x601020
0x400416 <__libc_start_main@plt+6>: push 0x1
从上面代码可以看出来跳转到0x40006(也就是PLT[2])也就是下一句再一次JMP跳转到了0x4003f0(PLT[0])然后在0x4003f0地址处push了GOT[1]再然后跳转到GOT[2](也就是加载动态链接器,GOT[2]跳转结束就会把函数的解析地址写入到Got表的对应位置)
=> 0x4003f0: push QWORD PTR [rip+0x200c12] # 0x601008
0x4003f6: jmp QWORD PTR [rip+0x200c14] # 0x601010
0x4003fc: nop DWORD PTR [rax+0x0]
0x400400 <puts@plt>: jmp QWORD PTR [rip+0x200c12] # 0x601018
0x400406 <puts@plt+6>: push 0x0
第二次调用:
=> 0x40052a <hello_world+4>: mov edi,0x4005e4
0x40052f <hello_world+9>: call 0x400400 <puts@plt>
mov语句和call语句合到了一起步入之后
=> 0x7ffff7a7c690 <_IO_puts>: push r12
0x7ffff7a7c692 <_IO_puts+2>: push rbp
0x7ffff7a7c693 <_IO_puts+3>: mov r12,rdi
0x7ffff7a7c696 <_IO_puts+6>: push rbx
0x7ffff7a7c697 <_IO_puts+7>: call 0x7ffff7a98720 <strlen>
直接调用了puts函数
这里存放的就是那些用到的函数的地址了
gdb-peda$ x/100c 0x601000
0x601000: 0x28 0xe 0x60 0x0 0x0 0x0 0x0 0x0
0x601008: 0x68 0xe1 0xff 0xf7 0xff 0x7f 0x0 0x0
0x601010: 0x70 0xe8 0xde 0xf7 0xff 0x7f 0x0 0x0
0x601018: 0x90 0xc6 0xa7 0xf7 0xff 0x7f 0x0 0x0
0x601020: 0x40 0xd7 0xa2 0xf7 0xff 0x7f 0x0 0x0
0x601028: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601030: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601038 <completed.7594>: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601040: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601048: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601050: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601058: 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x601060: 0x0 0x0 0x0 0x0
总结:分析了一下具体过程其实也不麻烦一共也没用几次跳转不过这个好像挺有用,先储备一下
推荐一篇博客讲这个的,我的也是大部分抄的
http://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html