GOT表和PLT表在程序中的作用非常巨大,接下来的讲解希望大家可以仔细看看
我们用一个非常简单的例子来讲解,代码如下:
图1
然后我们编译
我们直接gdb./a.out来进行反编译处理,然后通过disasmain查看main函数中的反编译代码如下:
图3
我们可以观察到gets@plt和puts@plt这两个函数,为什么后面加了个@plt,因为这个为PLT表中的数据的地址。那为什么反编译中的代码地址为PLT表中的地址呢。
原因
为了更好的用户体验和内存CPU的利用率,程序编译时会采用两种表进行辅助,一个为PLT表,一个为GOT表,PLT表可以称为内部函数表,GOT表为全局函数表(也可以说是动态函数表这是个人自称),这两个表是相对应的,什么叫做相对应呢,PLT表中的数据就是GOT表中的一个地址,可以理解为一定是一一对应的,如下图:
图4
其实在大家进入带有@plt标志的函数时,这个函数其实就是个过渡作用,因为GOT表项中的数据才是函数最终的地址,而PLT表中的数据又是GOT表项的地址,我们就可以通过PLT表跳转到GOT表来得到函数真正的地址。
那问题来了,这个@plt函数时怎么来的,这个函数是编译系统自己加的,大家可以通过disas gets
看看里面的代码,如下图:
图5
大家可以发现,这个函数只有三行代码,第一行跳转,第二行压栈,第三行又是跳转,解释:
第一行跳转,它的作用是通过
- jmp指令跳转到GOT表
- GOT表中的数据为0x400486
- 跳转到指令地址为0x400486
- 执行
push 0x3
#这个为在GOT中的下标序号 - 在执行
jmp 0x400440
- 而0x400440为PLT[0]的地址
- PLT[0]的指令会进入动态链接器的入口
- 执行一个函数将真正的函数地址覆盖到GOT表中
这里我们要提几个问题:
1. PLT[0]处到底做了什么,按照我们之前的思路它不是应该跳转到GOT[0]吗?
2. 为什么中间要进行push压栈操作?
3. 压入的序号为什么为0x3,不是最开始应该为0x0吗?
解决问题
问题1
看下图:
图6
我们尝试着查看disas gets
看代码的时候是从0x400440开始的,我们可以通过x /5i 0x400440
查看0x400440处的代码,如下:
图7
我们看到了后面的#之后又一个objdump -R ./a.out
查看一个程序的GOT函数的地址,如下图:
图8
这里都是些
咦,这里问题好像又产生了,本来按照最开始的思路PLT[1]也是跳转到GOT[1]的,GOT[2]同理,但是这两个数据好像被PLT[0]利用了,同时GOT[0]好像消失了,这里GOT[0]暂且不说它的作用是什么,针对GOT[1]和GOT[2]被PLT[0]利用,所以我们程序中真实情况其实是从PLT[1]到GOT[3],PLT[2]到GOT[4],所以我们推翻了我们的图4,建立一张新的处理表
图
而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x]中。
这就是PLT表和GOT表,总而言之,我们调用一个函数的时候有两种方法,一个是通过PLT表调用,一个则是通过GOT表调用,因为PLT表最终也是跳转GOT表,GOT表中则是一个函数真正的地址,这里需要注意的是,在一个函数没有运行一次之前,GOT表中的数据为@plt函数中下一条指令的地址,图5有说。
问题2
中间进行的压栈是为了确定
问题3
好像都在第一个问题都已经解决了,这里压入0x3的原因是因为,我们的GOT[0],GOT[1],GOT[2]都有额外用处。要从GOT[3]开始