Linux Debugging(七): 使用反汇编理解动态库函数调用方式GOT/PLT
本文主要讲解动态库函数的地址是如何在运行时被定位的。首先介绍一下PIC和Relocatable的动态库的区别。然后讲解一下GOT和PLT的理论知识。GOT是Global Offset Table,是保存库函数地址的区域。程序运行时,库函数的地址会设置到GOT中。由于动态库的函数是在使用时才被加载,因此刚开始GOT表是空的。地址的设置就涉及到了PLT,Procedure Linkage Table,它包含了一些代码以调用库函数,它可以被理解成一系列的小函数,这些小函数的数量其实就是库函数的被使用到的函数的数量。简单来说,PLT就是跳转到GOT中所设置的地址而已。如果这个地址是空,那么PLT的跳转会巧妙的调用_dl_runtime_resolve去获取最终地址并设置到GOT中去。由于库函数的地址在运行时不会变,因此GOT一旦设置以后PLT就可以直接跳转到库函数的真实地址了。最后使用反汇编验证和跳转流程图对上述结论加深理解。
1. 背景-PIC VS Relocatable
在 Linux 下制作动态链接库,“标准” 的做法是编译成位置无关代码(Position Independent Code,PIC),然后链接成一个动态链接库。那么什么是PIC呢?如果是非PIC的,那么会有什么问题?
(1) 可重定位代码(relocatable code):Windows DLL 以及不使用 -fPIC 的 Linux so。
生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址(base),这时要进行一次重定位(relocation),把代码、数据段中所有的地址加上这个 base 的值。这样代码运行时就能使用正确的地址了。当要再加载时根据加载到的位置再次重定位的。(因为它里面的代码并不是位置无关代码)。因为so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享。如果被多个应用程序共同使用,那么它们必须每个程序维护一份so的代码副本了。当然,主流现代操作系统都启用了分页内存机制,这使得重定位时可以使用
COW(copy on write)来节省内存(32 位 Windows 就是这样做的);然而,页面的粒度还是比较大的(例如 IA32 上是 4KiB),至少对于代码段来说能节省的相当有限。不能共享就失去了共享库的好处,实际上和静态库的区别并不大,在运行时占用的内存是类似的,仅仅是二进制代码占的硬盘空间小一些。
(2) 位置无关代码(position independent code):使用 -fPIC 的 Linux so。
这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。通常的方法是获取指令指针(如 x86 的 EIP 寄存器)的值,加上一个偏移得到全局变量/函数的地址。AMD64 下,必须使用位置无关代码。x86下,在创建so时会有一个警告。但是这样的so可以完全正常工作。PIC 的缺点主要就是代码有可能长一些。例如 x86,由于不能直接使用 [EIP+constant] 这样的寻址方式,甚至不能直接将 EIP 的值交给其他寄存器,要用到 GOT(global
offset table)来定位全局变量和函数。这样导致代码的效率略低。PIC 的加载速度稍快,因为不需要做重定位。多个进程引用同一个 PIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,但操作系统显然会把它们映射到同一块物理内存上。
因此,除非你的so不会被共享,否则还是加上-fPIC吧。
2. GOT和PLT
我们都知道动态库是在运行时绑定的。那么编译器是如何找到动态链接库里面的函数的地址呢?事实上,直到我们第一次调用这个函数,我们并不知道这个函数的地址,这个功能要做延迟绑定 lazy bind。 因为程序的分支很多,并不是所有的分支都能跑到,想想我们的异常处理,异常处理分支的动态链接库里面的函数也许永远跑不到,所以,启动时解析所有出现过的动态库里面的函数是个浪费的办法,降低性能并且没有必要。
Global Offset Table(GOT)
在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址;GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,后面我们会详细分析。GOT示意如下图,GOT表slot的数量就是3
+ number of functions to be loaded.
Procedure Linkage Table(PLT)
过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(如前所说,这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。
3. 反汇编
我们使用的代码是:
#include <iostream>
#include <stdlib.h>
void fun(int a)
{
a++;
} int main()
{
fun(1);
int x = rand();
return 0;
}
动态库里面需要重定位的函数在.got.plt这个段里面,通过readelf我们可以看到,它一共有六个地址空间,前三个我们已经解释了。说明该程序预留了三个所需要重新定位的函数。因此用不到的函数是永远不会被加载的。
[23] .dynamic DYNAMIC 0000000000600e10 00000e10
00000000000001d0 0000000000000010 WA 8 0 8
[24] .got PROGBITS 0000000000600fe0 00000fe0
0000000000000008 0000000000000008 WA 0 0 8
[25] .got.plt PROGBITS 0000000000600fe8 00000fe8
0000000000000048 0000000000000008 WA 0 0 8
反汇编main函数:
(gdb) disas main
Dump of assembler code for function main:
0x0000000000400549 <main+0>: push %rbp
0x000000000040054a <main+1>: mov %rsp,%rbp
0x000000000040054d <main+4>: sub $0x10,%rsp
0x0000000000400551 <main+8>: mov $0x1,%edi
0x0000000000400556 <main+13>: callq 0x40053c <fun>
0x000000000040055b <main+18>: callq 0x400440 <rand@plt>
0x0000000000400560 <main+23>: mov %eax,-0x4(%rbp)
0x0000000000400563 <main+26>: mov $0x0,%eax
0x0000000000400568 <main+31>: leaveq
0x0000000000400569 <main+32>: retq
End of assembler dump.
可以看到其实调用我们自定义的fun和系统库函数rand形成的汇编差不多,没有额外的处理。接着向下看rand:
(gdb) disas 0x400440
Dump of assembler code for function rand@plt:
0x0000000000400440 <rand@plt+0>: jmpq *0x200bc2(%rip) # 0x601008 <_GLOBAL_OFFSET_TABLE_+32>
0x0000000000400446 <rand@plt+6>: pushq $0x1
0x000000000040044b <rand@plt+11>: jmpq 0x400420
End of assembler dump.
真正有意思的在# 0x601008 <_GLOBAL_OFFSET_TABLE_+32>。也就是rand@plt首先会跳到这里。我们看一下这里是什么:
(gdb) x 0x601008
0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0x00400446
接着看0x00400446是什么:
(gdb) x/5i 0x00400446
0x400446 <rand@plt+6>: pushq $0x1
0x40044b <rand@plt+11>: jmpq 0x400420
可能你注意到了,这里的处理是和刚才的rand@plt的jmpq一样。都是将0x1入栈,然后jmpq 0x400420。因此这样就避免了GOT表是否为是真实值的检查:如果是空,那么去寻址;否则直接调用。
其实接下来处理的就是调用_dl_runtime_resolve_()函数,该函数最终会寻址到rand的真正地址并且会调用_dl_fixup来将rand的实际地址填入GOT表中。
我们将整个程序执行完,然后看一下0x601008 <_GLOBAL_OFFSET_TABLE_+32>是否已经修改成rand的实际地址:
(gdb) x 0x601008
0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0xf7ab6470
可以看到,rand的地址已经修改为0xf7ab6470了。然后可以通过maps确认一下是否libc load在这个地址:
(gdb) shell cat /proc/`pgrep a.out`/maps
00400000-00401000 r-xp 00000000 08:02 491638 /root/study/got/a.out
00600000-00601000 r--p 00000000 08:02 491638 /root/study/got/a.out
00601000-00602000 rw-p 00001000 08:02 491638 /root/study/got/a.out
7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7bd5000-7ffff7dd4000 ---p 00155000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd4000-7ffff7dd8000 r--p 00154000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd8000-7ffff7dd9000 rw-p 00158000 08:02 327685 /lib64/libc-2.11.1.so
7ffff7dd9000-7ffff7dde000 rw-p 00000000 00:00 0
7ffff7dde000-7ffff7dfd000 r-xp 00000000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7fc4000-7ffff7fc7000 rw-p 00000000 00:00 0
7ffff7ffa000-7ffff7ffb000 rw-p 00000000 00:00 0
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 0001e000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7ffd000-7ffff7ffe000 rw-p 0001f000 08:02 327698 /lib64/ld-2.11.1.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
没有问题,如我们所分析的那样:
7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so
以后的调用就直接调用库函数了:
尊重原创,转载请注明出处 anzhsoft: http://blog.csdn.net/anzhsoft/article/details/18776111
参考资料:
1. http://www.linuxidc.com/Linux/2011-06/37268.htm
2. http://blog.chinaunix.net/uid-24774106-id-3349549.html
3. http://www.linuxidc.com/Linux/2011-06/37268.htm
4. http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/
Linux Debugging(七): 使用反汇编理解动态库函数调用方式GOT/PLT的更多相关文章
- Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)
上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特 ...
- Linux Debugging(六): 动态库注入、ltrace、strace、Valgrind
实际上,Linux的调试方法非常多,针对不同的问题,不同的场景,不同的应用,都有不同的方法.很难去概括.本篇文章主要涉及本专栏还没有涵盖,但是的确有很重要的方法.本文主要包括动态库注入调试:使用ltr ...
- linux第七章读书笔记
Vim编辑器 Vim 仅仅通过键盘来在插入和执行命令等多种模式之间切换.这使得Vim可以不用进行菜单或者鼠标操作,并且最小化组合键的操作,对文字录入员或者程序员可以大大增强速度和效率. CHAPTER ...
- Linux Debugging(二): 熟悉AT&T汇编语言
没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用 ...
- Linux第七周学习总结——可执行程序的装载
Linux第七周学习总结--可执行程序的装载 作者:刘浩晨 [原创作品转载请注明出处] <Linux内核分析>MOOC课程http://mooc.study.163.com/course/ ...
- 【转帖】linux内存管理原理深入理解段式页式
linux内存管理原理深入理解段式页式 https://blog.csdn.net/h674174380/article/details/75453750 其实一直没弄明白 linux 到底是 段页式 ...
- Linux高级编程--02.gcc和动态库
在Linux环境下,我们通常用gcc将C代码编译成可执行文件,如下就是一个简单的例子: 小实验:hello.c #include <stdlib.h> #include <stdio ...
- Linux文件系统十问---深入理解文件存储方式(rhel6.5,EXT4)【转】
本文转载自:https://blog.csdn.net/tongyijia/article/details/52832236 前几天在红黑联盟上看了一篇博客<Linux文件系统十问—深入理解文件 ...
- [深入理解Android卷一全文-第七章]深入理解Audio系统
由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该由于纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容. ...
随机推荐
- Database Design Guidelines
Database Design Guidelines Principles Support popular databases Name Style Table Name Style: Pascal ...
- div英文内容超过div长度
添加css word-break: normal;word-wrap: break-word;
- 项目中BigDecimal与Double使用场景
金额要用BigDecimal 金额计算不能用doube!!!! 金额计算必须用BigDecimal,下面对比一下用double 跟BigDecimal的区别.先看一个小例子: 请看题: 示例1 问, ...
- pentaho cde 自定义复选下拉框 checkbox select
pentaho 自带的component 虽多,但是当用户需要在一个表格中查看多个组别的数据时,pentaho自带的单选框就不能实现了,所以复选下拉框势在必行,实现效果如下: 实现原理是借用了jqu ...
- 使用translate将字符串中的数字提取出来
--方法1: with tmp as ( select '按时的撒旦123元(其中含存款11元)' name from dual union all select '一类似的预存9600元(新势力)' ...
- python学习之路基础篇(三)
博客参考:http://www.cnblogs.com/wupeiqi/articles/4943406.html http://www.cnblogs.com/luotianshuai/p/4949 ...
- day06_JDBC学习笔记
============================================================ 一.JDBC概述 为什么要使用JDBC? JDBC:Java DataBase ...
- PHP 文件
PHP 文件处理 fopen() 函数用于在 PHP 中打开文件. 打开文件 fopen() 函数用于在 PHP 中打开文件. 此函数的第一个参数含有要打开的文件的名称,第二个参数规定了使用哪种模式来 ...
- springMVC源码分析--HandlerMethodArgumentResolver参数解析器(一)
HandlerMethodArgumentResolver是用来为处理器解析参数的,主要用在HandlerMethod中,每个Resolver对应一种类型的参数,其实现类特别的多. HandlerMe ...
- Xcode编译错误__NSCFConstantString
__NSCFConstantString:主要错误就是数据类型造成的,然后就是检查哪个地方造成的数据类型调用错误 错误一:'-[__NSCFConstantString _imageThatSuppr ...