首先寄存器使用惯例:
eip :指令地址寄存器,保存程序计数器的值,当前执行的指令的下一条指令的地址值,16位中为ip,32位为eip。eip不可以直接赋值,一般都是cpu自动加1来更新,指令call和ret以及jmp可以改变eip的值。
另外汇编代码格式有ATT和intel格式,gcc和objdump的默认格式就是ATT。几个小区别,1首先是指令ATT汇编指令后面有一个l,比如intel格式为mov,ATT格式为movl
2寄存器,ATT格式有%,比如intel格式为ebp,ATT格式为%ebp
3还有一个最主要的区别就是操作指令的,操作数的顺序是相反的,源操作数和目的操作数的顺序相反。
关于栈中的寄存器:
esp :栈指针,又叫栈顶寄存器,总是指向栈顶元素,已经压入栈的最顶上的那个元素,而不是待压入的。栈指针可以移动,通过上下移动实现栈的开辟和释放。栈是向小地址方向生长的。esp寄存器中保存的是当前栈的栈顶元素的地址。
ebp :帧指针,又叫栈基址寄存器,总是指向当前栈的栈底元素。保存的是当前栈的栈底元素的地址。帧指针不可以移动,用来作为当前栈的基址,通过对基址ebp的偏移来寻址访问栈中的其他元素的值。比如:0x8(%ebp)ebp所指向的地址值在加上0x8的地址。
ebp永远都是针对当前栈的,所以当一个函数调用了另外一个函数的时候,就需要先将调用函数的栈的ebp给入栈保存,这样避免了与被调用函数的栈ebp冲突,然后在被调用函数的栈结束调用释放的时候,恢复现场的时候,在将调用函数的ebp弹出来,这样又可以回到调用函数的栈了。
栈通过栈指针栈顶esp和帧指针栈底ebp来固定栈框。
eax :保存函数的返回值的惯用寄存器
% :直接寻址寄存器
( ) :内存间接寻址
$ :立即数
例如:movl $8, %eax #把立即数8存到寄存器eax中
movl $8, (%esp) #把立即数8存到内存esp所指的内存地址中。
这里主要来通过函数调用来彻底弄清楚栈的过程,这里直接利用bufbomb中的一段简单的c代码,通过反汇编来分析一下其汇编代码。这个c代码很简单,就是test函数中调用了getbuf函数。
其反汇编代码为:
这里的汇编代码比较长我们来截取一下只看关于栈和函数调用的部分:
来看一下不管是test还是getbuf都有的几句汇编语言:
函数的实现过程都是通过栈过程,一开始我们已经讲过了栈有两个指针用来固定栈框的,ebp栈底指针和而esp栈顶指针。
push %ebp #保存旧的ebp的值,也就是保存当前函数的调用者的栈的栈基址。(因为每一个栈都有一个ebp是不可以移动的,但是名字又一样,怎么区分呢,那就是开始时先把原来的ebp保存起来,然后生成自己的,调用结束后,在把原来的恢复,弹出来,自己的释放掉,这样人家原来的ebp又可以继续在自己的栈中作为基址了。不然就覆盖了回不去了)相对于test来说可能就是main之类的调用test的函数的栈的ebp,对于getbuf来说就是test的ebp。将ebp压入栈中,这个时候esp自动-4,指向新压入的ebp元素的位置处。
mov %esp,%ebp #使帧指针ebp指向当前的esp处,也就是初始化生成一个当前栈的基址帧指针ebp,也就是当前栈的栈底表示出来固定住。对于test来说就是test的栈底,对于getbuf来说就是getbuf的栈的栈底。
sub $0x38,%esp #通过栈指针esp向下移动为当前函数开辟自己的栈,esp指向了栈顶,从ebp到esp为当前函数的栈空间。
以上三条指令就是所有栈都有的栈开辟汇编指令。
接下来来看栈释放的汇编指令:
这里是两个函数中不同的栈释放恢复指令,有点不一样但是原理是一样的。
add $0x24,%esp #释放当前栈开辟的空间,在test函数开始的时候通过栈指针esp减0x24移动来开辟了当前test函数自己的栈空间。现在将esp加上0x24也就是使esp从新移动到了栈底ebp处,就将原来开辟的栈空间释放掉了。另外一条常用的汇编指令是:
mov %ebp,%esp #将ebp的值给esp,也就是把esp指向当前的栈底
pop %ebx #把之前保存的寄存器ebx恢复
pop %ebp #弹出旧的ebp,也就是把调用test函数的函数的栈的ebp弹出恢复。
ret # 弹出返回地址。这一条指令相当于
pop %eip 将指令寄存器恢复,也就是让调用test函数的函数的栈知道接下来应该继续执行的哪一条指令。
leave # 是将当前栈的空间释放掉,弹出旧的ebp,相当于下面两条汇编指令:
mov %ebp,%esp
pop %ebp
由此可见栈的释放就是三个过程:
释放当前栈的空间
弹出旧的ebp
弹出返回地址
过程与esp的移动结合:(有很多人不明白,我这里已经用自己的话写的非常白话了,我觉得最后的理解方式是通过gdb调试一下,那里不明白就调试出来里面到底是什么就会自己豁然开朗)
栈释放主要包括:1将当前开放的栈空间释放掉,通过esp的向上移动,移动到自己的栈底ebp处(保存的旧的调用者的ebp的位置处),2恢复原来的现场,主要包括两部分,一部分是将原来的栈基址帧指针ebp复原,也就是在自己栈中保存的ebp弹出来,这个时候esp自动加4,变到原来保存旧ebp处位置上上面一个位置(一般为调用者的栈中保存的返回地址的地方,返回地址是调用当前函数时,结束后回来继续应该执行的下一条指令的eip的地址。比如call指令的下一条指令的eip的地址
比如说上面test中call getbuf这条eip的下一条eip的地址为0x8048e50
在执行call指令的时候相当于:
push eip(0x8048e50) (系统自动将返回地址压入栈,输入调用者的一部分,比如这里在test的栈中,而getbuf的栈是从getbuf压入test的ebp开始,也就是getbuf的栈底元素是test的ebp)
jmp getbuf(0x8049262)
),这个时候保存的旧的ebp已经弹出,之前为当前栈生成的栈基址帧指针ebp也就没有了已经。也就是被调用函数开辟的栈已经完全复原了,像什么没有发生一样。第二个部分也是最后一个部分,就是调用者需要知道我应该继续执行那一条指令(不然回来了找不到原来的指令执行到哪里了),也就是把返回地址eip的地址弹出来,esp自动加4指向原来保存返回地址的位置上面一个位置。这样调用函数又继续正常执行了。
图解函数调用过程:
1首先是test函数的栈结构,其中黄色是test函数的栈,绿色是调用test函数的函数的栈,比如main函数之类的。
2 test执行到call指令:
call 8049262 <getbuf>
首先 系统自动压入返回地址 push eip 这里call的下一条eip的地址是0x8048e50
然后 Jmp到jmp getbuf(0x8049262)
随着返回地址的入栈,esp自动下移,esp-4:这个时候仍是黄色的,因为我们在前面已经分析过,返回地址是属于调用者的栈结构的。
3跳到getbuf的函数的入口地址以后开始getbuf的栈,蓝色的代表getbuf的栈
push %ebp #保存旧的ebp的值,也就是保存当前函数的调用者的栈的栈基址。对于getbuf来说就是test的ebp。将ebp压入栈中,这个时候esp自动-4,指向新压入的ebp元素的位置处。
mov %esp,%ebp #使帧指针ebp指向当前的esp处,也就是初始化生成一个当前栈getbuf栈的基址帧指针ebp,也就是当前栈的栈底表示出来固定住。对于getbuf来说就是getbuf的栈的栈底。
4 getbuf继续栈开辟
sub $0x38,%esp 通过esp移动开辟一个getbuf的栈空间,esp此时指向getb这个栈的栈顶,此时getbuf的栈框已经固定住。
接下来再来看函数调用完以后返回到test函数,现场恢复:
1leave的第一步:
mov %ebp,%esp #将ebp的值给esp,也就是把esp指向当前的栈底,把开辟的蓝色空间收回
2 leave的第二步:
pop %ebp #弹出旧的ebp,也就是把调用test函数的函数的栈的ebp弹出恢复。此时蓝色框已经完全没有了,ebp也没有了,为了getbuf开的空间也已经完全释放了。
3 ret 弹出返回地址以后:
这里以上仅是简单的函数调用,调用的函数不需要传入参数,还有调用的函数需要传入参数的时候等,在bufbomb中我们会具体遇到。再具体分析。