本文主要讲解动态库函数的地址是如何在运行时被定位的。首先介绍一下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的更多相关文章

  1. Linux Debugging(三): C++函数调用的参数传递方法总结(通过gdb+反汇编)

    上一篇文章<Linux Debugging:使用反汇编理解C++程序函数调用栈>没想到能得到那么多人的喜爱,因为那篇文章是以32位的C++普通函数(非类成员函数)为例子写的,因此只是一个特 ...

  2. Linux Debugging(六): 动态库注入、ltrace、strace、Valgrind

    实际上,Linux的调试方法非常多,针对不同的问题,不同的场景,不同的应用,都有不同的方法.很难去概括.本篇文章主要涉及本专栏还没有涵盖,但是的确有很重要的方法.本文主要包括动态库注入调试:使用ltr ...

  3. linux第七章读书笔记

    Vim编辑器 Vim 仅仅通过键盘来在插入和执行命令等多种模式之间切换.这使得Vim可以不用进行菜单或者鼠标操作,并且最小化组合键的操作,对文字录入员或者程序员可以大大增强速度和效率. CHAPTER ...

  4. Linux Debugging(二): 熟悉AT&T汇编语言

    没想到<Linux Debugging:使用反汇编理解C++程序函数调用栈>发表了收到了大家的欢迎.但是有网友留言说不熟悉汇编,因此本书列了汇编的基础语法.这些对于我们平时的调试应该是够用 ...

  5. Linux第七周学习总结——可执行程序的装载

    Linux第七周学习总结--可执行程序的装载 作者:刘浩晨 [原创作品转载请注明出处] <Linux内核分析>MOOC课程http://mooc.study.163.com/course/ ...

  6. 【转帖】linux内存管理原理深入理解段式页式

    linux内存管理原理深入理解段式页式 https://blog.csdn.net/h674174380/article/details/75453750 其实一直没弄明白 linux 到底是 段页式 ...

  7. Linux高级编程--02.gcc和动态库

    在Linux环境下,我们通常用gcc将C代码编译成可执行文件,如下就是一个简单的例子: 小实验:hello.c #include <stdlib.h> #include <stdio ...

  8. Linux文件系统十问---深入理解文件存储方式(rhel6.5,EXT4)【转】

    本文转载自:https://blog.csdn.net/tongyijia/article/details/52832236 前几天在红黑联盟上看了一篇博客<Linux文件系统十问—深入理解文件 ...

  9. [深入理解Android卷一全文-第七章]深入理解Audio系统

    由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该由于纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容. ...

随机推荐

  1. Java Web -【分页功能】详解

    分页简介 分页功能在网页中是非常常见的一个功能,其作用也就是将数据分割成多个页面来进行显示. 使用场景: 当取到的数据量达到一定的时候,就需要使用分页来进行数据分割. 当我们不使用分页功能的时候,会面 ...

  2. 重新设置Eclipse的workspace路径

    有3中方法可以更改workspace的路径设置: 1. 启动Eclipse/MyEclipse后, 打开"Window -> Preferences -> General -&g ...

  3. Jackson工具

    Jackson Jackson包含一个core JAR,和两个依赖core JAR的JAR: jackson-core-2.2.3.jar(核心jar包,下载地址) jackson-annotatio ...

  4. 利用Express模拟web安全之---xss的攻与防

    一.什么是XSS? 跨站脚本攻击(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS.恶意 ...

  5. android 欢迎界面的制作

    再打开手机app的时候,最先映入我们眼帘的是一个覆盖手机全屏的欢迎界面,在这个界面显示出来的时候整个手机屏幕只会显示这一个界面,上面的标题栏,以及手机最顶端的状态栏都会消失,只有欢迎页面结束跳转到其他 ...

  6. 解决Mysql数据库拒绝远程连接和忘记密码的问题

    解决数据库忘记密码的问题 ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) 编辑m ...

  7. Java不走弯路教程(1.环境搭建)

    1.环境搭建在开始写第一个Java程序之前,我们需要做一些简单的准备工作. 1.1 还记得DOS吗 我们可以通过图形界面来操作我们的电脑.但作为程序员,你首先需要学会用命令行的方式来操作电脑,因为不是 ...

  8. Node.js 教程

    简单的说 Node.js 就是运行在服务端的 JavaScript. Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台. Node.js是一个事件驱动I/O服务端Ja ...

  9. Java中Semaphore(信号量)的使用

    Semaphore的作用: 在java中,使用了synchronized关键字和Lock锁实现了资源的并发访问控制,在同一时间只允许唯一了线程进入临界区访问资源(读锁除外),这样子控制的主要目的是为了 ...

  10. mysql和postgresql转义字符探究

    总结 mysql依靠反斜杠\转义, postgresql 依靠单引号转义 mysql 客户端 mysql> create table usr (name varchar(), age integ ...