从《
Linux应用程序elf描述》我们讲到了PLT,但并没有深入理解,因此本篇主要介绍PLT是怎么工作的。
那么什么是PLT?PLT是一种Linux实现的延迟加载技术,据我了解windows不带这个技能,windows是程序启动的时候进行符号重定向的。而符号重定向是指可执行程序在编译的时候,不能提前知道对应函数、全局变量符号的具体地址,这个地址通常位于动态库当中。因此,需要在加载动态库之后,才能知道它的具体地址。故而,编译器在编译的时候,通常只留一个占位符给对应地址,这需要在应用程序启动的时候,由动态解释器对这种没有具体地址的符号进行新地址填写和查找的过程 叫做符号重定位。既然有了符号重定向,PLT拿来又有什么用? 当我们在启动一个拥有几千上万动态库的应用程序的时候,如果所有函数符号都要在启动的时候去重定位它的实际地址,那么这需要多长时间呢?因此,Linux为了节约程序的启动耗时,采用了PLT技术,所谓PLT技术就是指所有的函数的地址重定位不是在启动的时候完成, 而是在具体函数调用的时候完成,这样启动时间就大大加快了。
我们以hello world程序为例,如下:
-
#include <stdio.h>
-
-
int main (int argc, char *argv[])
-
{
-
printf ("Hello World\n");
-
-
return 0;
-
}
执行:gcc -g hello.c -o hello 生成hello可执行文件(可以GDB调试的ELF文件格式).
咋们先看看这个hello程序的代码段部分和.plt段部分,如下:
-
Disassembly of section .plt:
-
-
00000000004003f0 <puts@plt-0x10>:
-
4003f0: ff 35 12 0c 20 00 pushq 0x200c12(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
-
4003f6: ff 25 14 0c 20 00 jmpq *0x200c14(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
-
4003fc: 0f 1f 40 00 nopl 0x0(%rax)
-
-
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>
-
-
0000000000400410 <__libc_start_main@plt>:
-
400410: ff 25 0a 0c 20 00 jmpq *0x200c0a(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
-
400416: 68 01 00 00 00 pushq $0x1
-
40041b: e9 d0 ff ff ff jmpq 4003f0 <_init+0x28>
-
-
Disassembly of section .text:
-
-
.....省略其他函数......
-
-
0000000000400526 <main>:
-
400526: 55 push %rbp
-
400527: 48 89 e5 mov %rsp,%rbp
-
40052a: 48 83 ec 10 sub $0x10,%rsp
-
40052e: 89 7d fc mov %edi,-0x4(%rbp)
-
400531: 48 89 75 f0 mov %rsi,-0x10(%rbp)
-
400535: bf d4 05 40 00 mov $0x4005d4,%edi
-
40053a: e8 c1 fe ff ff callq 400400 <puts@plt>
-
40053f: b8 00 00 00 00 mov $0x0,%eax
-
400544: c9 leaveq
-
400545: c3 retq
-
400546: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
-
40054d: 00 00 00
如上图,我们注意带颜色部分,通过hello.c,我们可以看出,我们实际是在main函数当中调用了printf函数,然后main函数返回。但是通过汇编代码,我们看到在main函数当中,并没有printf函数,这是为什么呢?因为我们的printf函数只传入了字符串,这个写法默认会被编译器优化,优化成有puts函数代替printf来打印字符串,如果有其他可变参数传入,则会直接用printf,而不会被优化(有兴趣的可以自己试试)。
从上图,我们可以了解到字符串"Hello world"最终是由main函数调用puts函数来打印的。但是实际从汇编来看,main函数并没有直接去call puts函数,而是调用的puts@plt。请注意puts@plt并不是puts函数,这里不要搞混了,它只是一个跳板函数。我们知道puts函数的具体实现是在libc.so当中,因此这里引入的puts@plt函数,实际就是我们的PLT技术(程序刚开始运行的时候,由于启动并没有对puts函数的地址进行重定位,因此不能直接去call puts函数,而必须要经过跳板函数puts@plt去转换一下)。
如上图29行,main函数调用puts@plt函数,而puts@plt函数在第8行实现,它也是一个函数,只是的实体在.plt段当中,它的作用就是帮助我们的hello程序去定位真正的puts函数。下面我们来详细解说这个puts@plt的运行过程:
-
00000000004003f0 :
-
4003f0: ff 35 12 0c 20 00 pushq 0x200c12(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
-
4003f6: ff 25 14 0c 20 00 jmpq *0x200c14(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
-
4003fc: 0f 1f 40 00 nopl 0x0(%rax)
-
-
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>
如上图,_GLOBAL_OFFSET_TABLE_实际上就是PLT GOT表的基地址即.got.plt段的地址0x601000,我们通过objdump -s hello 可以获取到,下面是.got.plt段的部分数据:
-
Contents of section .got:
-
600ff8 00000000 00000000 ........
-
Contents of section .got.plt:
-
601000 280e6000 00000000 00000000 00000000 (.`.............
-
601010 00000000 00000000 06044000 00000000 ..........@.....
-
601020 16044000 00000000 ..@.....
-
Contents of section .data:
-
601028 00000000 00000000 00000000 00000000 ................
注意:RIP指针始终指向下一条指令
我们看到puts@plt的第一行汇编,jmpq *0x601018
(0x601018 = %rip+0x200c12 = 0x4003f6 + 0x200c12),而0x601018这个地址是属于.got.plt段的,.got.plt段中红色部分就是0x601018的数据部分,即0x400406(注意大小端),这个地址就是puts@plt的第二行汇编,也就是main函数跳转到puts@plt函数运行,然后puts@plt间接跳转到0x400406也就是pushq $0x0(黄色标记部分),最后通过jmpq 0x4003f0跳转到0x4003f0(黄色标记部分)去运行,并执行0x601010上的数据。我们用gdb实际看看运行效果:
-
$gdb hello
-
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
-
Copyright (C) 2016 Free Software Foundation, Inc.
-
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
-
This is free software: you are free to change and redistribute it.
-
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
-
and "show warranty" for details.
-
This GDB was configured as "x86_64-linux-gnu".
-
Type "show configuration" for configuration details.
-
For bug reporting instructions, please see:
-
<http://www.gnu.org/software/gdb/bugs/>.
-
Find the GDB manual and other documentation resources online at:
-
<http://www.gnu.org/software/gdb/documentation/>.
-
For help, type "help".
-
Type "apropos word" to search for commands related to "word"...
-
Reading symbols from hello...done.
-
(gdb) b main 在main函数入口设置一个断点
-
Breakpoint 1 at 0x400535: file hello.c, line 5.
-
(gdb) r 运行hello程序
-
Starting program: /ctu-nightly02/buildarea/cliu4/c-test/hello
-
-
Breakpoint 1, main (argc=1, argv=0x7fffffffe448) at hello.c:5
-
5 printf ("Hello World\n");
-
(gdb) x 0x400400 此时,还没有重定位puts函数,查看0x400400的值,发现是puts@plt函数
-
0x400400 <puts@plt>: 0x0c1225ff
-
(gdb) x 0x601018 此时,还没有重定位puts函数,查看0x601018的值,发现是0x00400406地址,即puts@plt函数的第二行汇编
-
0x601018: 0x00400406
-
(gdb) x 0x601010 继续往下执行,看最终跳转地址,存放到0x601010地址上,为0xf7deeee0是_dl_runtime_resolve函数
-
0x601010: <_dl_runtime_resolve>: 0xf7deeee0
-
(gdb) n 我们调过这一步运行,让它完整执行_dl_runtime_resolve函数
-
Hello World
-
7 return 0;
-
(gdb) x 0x601018 我们再次查看0x601018地址,发现上面存的数据变化,变为0xf7a7c690,即为puts函数的实际地址.
-
0x601018: <puts>: 0xf7a7c690
-
(gdb) quit
-
A debugging session is active.
-
-
Inferior 1 [process 307928] will be killed.
-
-
Quit anyway? (y or n) y
从上图可以看到,当以后再次想调用puts函数的时候, 就不需要再次经过这些步骤了,因为地址0x601018不在记录它的下一行汇编代码,而是变成了真正的puts函数地址,一个偷梁换柱就这样完成了。
_dl_runtime_resolve函数这里不做详细说明, 有兴趣的可以去C库查找对应实现,这个函数的功能就是去重定位对应C库中的函数的真实虚拟地址的,最后做一些操作,覆盖对应.got.plt的数据,然后返回。整个执行流程大概如下:
-
第一次调用puts函数:<main> --> <puts@plt> --> <puts@plt-0x10> --> <_dl_runtime_resolve> --> <puts>
-
第二次调用puts函数:<main> --> <puts@plt> --> <puts>
注:以上调用,中间采用GOT表(一张线性数组)来决定。
阅读(3401) | 评论(0) | 转发(0) |