本文主要讲解动态库函数的地址是如何在运行时被定位的。首先介绍一下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. JVM回收方法区内存

    很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是 ...

  2. Python里面 search0和 match0的区别?

    这是正则表达式里面的函数: match()函数只检测RE是不是在string的开始位置匹配,search()会扫描整个string查找匹配: 也就是说match()只有在0位置匹配成功的话才有返回,如 ...

  3. mybatis choose标签的使用

    MyBatis 提供了 choose 元素.if标签是与(and)的关系,而 choose 是或(or)的关系. choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立 ...

  4. bash的工作特性及其使用方法

    bash的工作特性之命令执行状态返回值和命令展开所涉及的内容及其示例演出 !脚本执行与调试1.绝对路径执行,要求文件有执行权限2.以sh命令执行,不要求文件有执行权限3..加空格或source命令执行 ...

  5. Python中什么是变量Python中定义字符串

    在Python中,变量的概念基本上和初中代数的方程变量是一致的. 例如,对于方程式 y=x*x ,x就是变量.当x=2时,计算结果是,当x=5时,计算结果是25. 只是在计算机程序中,变量不仅可以是数 ...

  6. Docker其它安全特性

    除了能力机制之外,还可以利用一些现有的安全机制来增强使用 Docker 的安全性,例如 TOMOYO, AppArmor, SELinux, GRSEC 等. Docker 当前默认只启用了能力机制. ...

  7. TensorFlow入门和示例分析

    本文以TensorFlow源码中自带的手写数字识别Example为例,引出TensorFlow中的几个主要概念.并结合Example源码一步步分析该模型的实现过程. 一.什么是TensorFlow 在 ...

  8. Mysql 统一设置utf8字符

    无聊的关于有效配置文件路径的备忘 原来阿里云服务器的mysql 5.5 , 配置/etc/my.cnf是没有任何作用的,需要编辑/etc/mysql/my.cnf 妈的, 就是这一点让我测试了两天, ...

  9. Bootstrap3 栅格系统-简介

    Bootstrap 提供了一套响应式.移动设备优先的流式栅格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列.它包含了易于使用的预定义类,还有强大的mixin 用于生成更具 ...

  10. Java经典设计模式之十一种行为型模式(附实例和详解)

    Java经典设计模式共有21中,分为三大类:创建型模式(5种).结构型模式(7种)和行为型模式(11种). 本文主要讲行为型模式,创建型模式和结构型模式可以看博主的另外两篇文章:Java经典设计模式之 ...