我们简单做个试验研究下PLT和GOT。下面是一段简单的代码,我们将其编译成动态库
libadd.so
#include <cstdio>
#include <cmath>
static int myadd(const int a, const int b){
return a + b;
int myabs(const int a){
return std::abs(a);
void test(const int a, const int b){
printf("%d %d", myadd(a, b), myabs(a));
下面是生成的动态库的反汇编:
0000000000000630 <_Z5myabsi>:
630: 55 push %rbp
631: 48 89 e5 mov %rsp,%rbp
634: 89 7d fc mov %edi,-0x4(%rbp)
637: 8b 45 fc mov -0x4(%rbp),%eax
63a: 89 c1 mov %eax,%ecx
63c: f7 d9 neg %ecx
63e: 0f 49 c1 cmovns %ecx,%eax
641: 5d pop %rbp
642: c3 retq
643: 66 66 66 66 2e 0f 1f data16 data16 data16 nopw %cs:0x0(%rax,%rax,1)
64a: 84 00 00 00 00 00
0000000000000650 <_Z4testii>:
650: 55 push %rbp
651: 48 89 e5 mov %rsp,%rbp
654: 48 83 ec 10 sub $0x10,%rsp
658: 89 7d fc mov %edi,-0x4(%rbp)
65b: 89 75 f8 mov %esi,-0x8(%rbp)
65e: 8b 7d fc mov -0x4(%rbp),%edi
661: 8b 75 f8 mov -0x8(%rbp),%esi
664: e8 27 00 00 00 callq 690 <_ZL5myaddii>
669: 89 45 f4 mov %eax,-0xc(%rbp)
66c: 8b 7d fc mov -0x4(%rbp),%edi
66f: e8 bc fe ff ff callq 530 <_Z5myabsi@plt>
674: 8b 75 f4 mov -0xc(%rbp),%esi
677: 89 c2 mov %eax,%edx
679: 48 8d 3d 2d 00 00 00 lea 0x2d(%rip),%rdi # 6ad <_fini+0x9>
680: b0 00 mov $0x0,%al
682: e8 99 fe ff ff callq 520 <printf@plt>
687: 48 83 c4 10 add $0x10,%rsp
68b: 5d pop %rbp
68c: c3 retq
68d: 0f 1f 00 nopl (%rax)
0000000000000690 <_ZL5myaddii>:
690: 55 push %rbp
691: 48 89 e5 mov %rsp,%rbp
694: 89 7d fc mov %edi,-0x4(%rbp)
697: 89 75 f8 mov %esi,-0x8(%rbp)
69a: 8b 45 fc mov -0x4(%rbp),%eax
69d: 03 45 f8 add -0x8(%rbp),%eax
6a0: 5d pop %rbp
6a1: c3 retq
从上面能够看到对于内部函数的调用直接使用的内部偏移,比如myadd2
中调用myadd
就是callq 690 <_ZL5myaddii>
。而调用printf
和myabs
就是callq 520 <printf@plt>
和callq 530 <_Z5myabsi@plt>
。
下来我们分析下这个跳转指令。e8表示偏移跳转,后面跟的就是跳转地址偏移,即0xfffffebc
,实际跳转地址便是off + rip=0xfffffebc + 674=0x530
。(需要注意的是执行 callq 指令之前,RIP 指向 callq 指令的下一条指令,因此RIP是674)。
66f: e8 bc fe ff ff callq 530 <_Z5myabsi@plt>
接下来我们找到0x530
的地址能够看到该地址又跳转到了510即plt表项,最终跳转到0x200af2(%rip)
从注释中能够看到是GOT的表项。
0000000000000510 <.plt>:
510: ff 35 f2 0a 20 00 pushq 0x200af2(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
516: ff 25 f4 0a 20 00 jmpq *0x200af4(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
51c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000520 <printf@plt>:
520: ff 25 f2 0a 20 00 jmpq *0x200af2(%rip) # 201018 <printf@GLIBC_2.2.5>
526: 68 00 00 00 00 pushq $0x0
52b: e9 e0 ff ff ff jmpq 510 <.plt>
0000000000000530 <_Z5myabsi@plt>:
530: ff 25 ea 0a 20 00 jmpq *0x200aea(%rip) # 201020 <_Z5myabsi@@Base+0x2009f0>
536: 68 01 00 00 00 pushq $0x1
53b: e9 d0 ff ff ff jmpq 510 <.plt>
接下来要查看GOT需要运行时查看,我们用GDB调试即可。首先在调用myabs
的地方断点,单步进入,可以看到当前的代码:
(gdb) x /10i $pc
=> 0x7fffff1f0530 <_Z5myabsi@plt>: jmpq *0x200aea(%rip) # 0x7fffff3f1020
0x7fffff1f0536 <_Z5myabsi@plt+6>: pushq $0x1
0x7fffff1f053b <_Z5myabsi@plt+11>: jmpq 0x7fffff1f0510
0x7fffff1f0540 <__cxa_finalize@plt>: jmpq *0x200a9a(%rip) # 0x7fffff3f0fe0
0x7fffff1f0546 <__cxa_finalize@plt+6>: xchg %ax,%ax
从上面的代码中能够看到需要跳转的地址是RIP+off=0x7fffff1f0536+0x200aea=0x7fffff3f1020
。从下面的内容可以看到这个地址存储的是当前指令下一条执行的地址,即0x7fffff1f0536
,也就是说这不是真正的函数地址还没有重定位。而上面的push $0x1
就是预期这个符号在plt中的槽位编号。
(gdb) x /gx 0x7fffff3f1020
0x7fffff3f1020: 0x00007fffff1f0536
(gdb) x /gx 0x00007fffff1f0536
0x7fffff1f0536 <_Z5myabsi@plt+6>: 0xffd0e90000000168
我们再单步几次就能看到基本能够确认这个过程是在进行符号解析:
(gdb) si
_dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:71
71 ../sysdeps/x86_64/dl-trampoline.h: No such file or directory.
(gdb) x /3i $pc
=> 0x7fffff4178f0 <_dl_runtime_resolve_xsavec>: push %rbx
0x7fffff4178f1 <_dl_runtime_resolve_xsavec+1>: mov %rsp,%rbx
0x7fffff4178f4 <_dl_runtime_resolve_xsavec+4>: and $0xffffffffffffffc0,%rsp
退出当前函数,我们再看PLT表中的表项,可以看到已经被修改为_Z5myabsi
的函数地址了。
(gdb) disass '_Z5myabsi@plt'
Dump of assembler code for function _Z5myabsi@plt:
0x00007fffff1f0530 <+0>: jmpq *0x200aea(%rip) # 0x7fffff3f1020
0x00007fffff1f0536 <+6>: pushq $0x1
0x00007fffff1f053b <+11>: jmpq 0x7fffff1f0510
End of assembler dump.
(gdb) x /gx 0x7fffff3f1020
0x7fffff3f1020: 0x00007fffff1f0630
(gdb) x /gx 0x00007fffff1f0630
0x7fffff1f0630 <_Z5myabsi>: 0x8bfc7d89e5894855
PLT 和 GOT 是现代动态链接的核心机制,通过延迟绑定和地址无关性,提升了动态库的加载效率和灵活性。这些机制确保了代码复用及共享的优势,同时优化了性能。
今天来学习GOT表和PLT表
操作系统通常使用动态链接的方法来提高程序运行的效率。在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么就不会再程序中被加载进来。
现代操作系统不允许修改代码段,只能修改数据段,那么GOT表与PLT表就应运而生。
我们知道linux为了降低可执行程序体积,提高空间利用效率,经常采用动态链接方式生成动态库或者可执行程序。在动态链接时,为了避免在加载时对代码段进行重定位导致动态库代码段无法实现共享,我们采用了位置无关代码PIC技术(Position-Independent Code)。针对模块外代码,为了实现PIC技术,我们需要借助全局偏移表(GOT,Gobal Offset Table)。
全局偏移表GOT
首先写一个小的测试程序进行说明。在SendMessage.c文件中实现了一个函数SendMessag
函数在类中被称作方法(methods)
主要目的:防止大量代码重复。在需要复制粘贴大量代码的地方改为使用函数可提高效率、减少错误。
太多的函数调用会减慢运行速度(非内联(inline)函数时):每次调用函数,编译器产生一个调用指令。
为了调用一个函数,需要为该函数创建一个栈框架(stack frame),将参数、返回地址等推(push)到栈上;然后跳到程序的相应位置,执行那个函数;函数执行完后返回保存在栈上的数据,回到调用函数的地方。
因此反复地调用函数
程序对于外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,(C语言应该讲过)链接的方式分为静态链接和动态链接。
静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件中并不包含外部函数的代码,而是运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。
怎么理解呢,这里打个比喻,静态链接是给你一件成品武器,调用时直接整个武器都已经组装好了到你手里,动
为了更好的用户体验
和内存CPUCPU的利用率,程序编译时会采用两种
表进行辅助,一个为
PLTPLT表,一个为
GOTGOT表,
PLTPLT表可以称为内部函数
表,
GOTGOT表为全局函数
表。
PLTPLT表中的每一项的数据内容都是对应的
GOTGOT表中一项的地址这个是固定不变的,到这里大家也知道了
PLTPLT表中的数据根本不是函数的真实地址,而是
GOTGOT表项的地址。
Global Offset Table(GOT)
在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。
因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的