这是大佬Cyberangl的文章的笔记,原文链接pwn知识库
大佬懒我也好懒,先说一下,笔记里的%ebp都是使用的AT&T语法(这是啥我还不知道),指的是ebp寄存器。
栈是什么
栈是一种LIFO(last-in,first-out)形式的数据结构,所有的数据都是后进先出。这种形式正好满足我们调用函数的方式:父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基础操作,push和pop。push将数据压入栈中,pop将栈中数据弹出并存储到指定寄存器或内存中。
实例:假设有了一个栈,其中黄色部分是以经写入的区域,绿色部分是还未写入数据的区域。
push 0x50 //把0x50压入栈中
pop操作:
pop 寄存器名称 //将栈中 的0x50弹出到某个寄存器中
注意:
一、上面的例子中栈的生长方向是从高地址到低地址的,对应笔记图片中栈是向下生长的。
二、pop操作之后,栈中的数据没有被清空,只是该数据我们无法直接访问,但是还是可以访问的。
具有以上栈基础后,下面是x86-32bit系统下c语言的函数调用。
栈帧是什么
stack frame,本质是一种栈,该栈专门用于保存函数调用过程中的各种信息(参数(实参)、返回地址、本地变量)。栈帧有栈顶和栈底之分,栈顶的地址最低,栈底的地址最高,SP(栈指针)是一直指向栈顶的。 在下6
-32bit中,我们用ebp指向栈底、用esp指向栈顶。 下面是栈帧示意图:
一般,我们将ebp
到esp
之间的区域当作栈帧。并不是 整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。 在函数调用中 ,调用函数的函数称为“调用者(caller)”,被调用的函数叫做“被调用者“(callee)”。
在函数调用过程中:
(1)”调用者“需要知道在哪里获取”被调用者“返回的值。
(2)”被调用者“需要知道传入的参数在哪。
同时还要保证”被调用者“返回后,ebp
,esp
等寄存器的值应该和调用前一致。因此,我们需要用栈来存储这些数据。
函数调用实例
函数的调用
int MyFunction(int x,int y,int z)
{
int a,b,c;
a=10;
b-5;
c=2;
...
return;
}
int TestFunction()
{
MyFunction(1,2,3);
...
}
当调用这个函数时,MyFunction()
的汇编代码大致如下:
_MyFunction:
push ebp ;//保存ebp的值
mov ebp,esp ;//将esp的值赋给ebp,使新的ebo指向栈顶
sub esp,0x12 ;//分配额外的空间给本地变量
mov qword ptr [ebp-4],10 ;//对栈中内存进行赋值操作
mov qword ptr [ebp-8],5 ;//对栈中内存进行赋值操作
mov qword ptr [ebp-12],2 ;//对栈中内存进行赋值操作
图解:
此时调用者做了两件事,
第一,将被调用者函数的参数压入栈中。
第二、将返回地址压入栈中。
这两件使都是调用者负责的,因此压入的栈应该属于调用者的栈帧。
再看被调用者,同样做了两件事。
第一、将原来的(调用者的)ebp
压入栈中,此时esp
指向它/
第二、将esp
的值赋给ebp
,ebp
就有了新的值,它也指向存放在原来ebp
的栈空间。这时他成了是函数MyFunction()
栈帧的栈底。这样,我们就保存了”调用者“函数的ebp
,并建立了一个新的栈帧。
在ebp
更新后,物品们先分配出一块0x12字节的空间用于存放本地变量,这步使用sub
实现。通过使用mov转移指令
配合字节数 ptr [offset]
我们便可以给a
,b
,c
赋值。
函数的返回
和函数调用时正好相反。当函数完成任务后,它会将esp
移到ebp
处,然后再弹出原来的ebp
的值到ebp寄存器
。这样ebp
就恢复到了函数调用之前的状态了。
int MyFunction(int x,int y,int z)
{
int a,b,c;
a=10;
b-5;
c=2;
...
return;
}
其汇编大致如下:
_MyFunction:
push ebp
mov ebp,esp
...
mov esp,sbp
pop ebp
ret
对ret
指令的解释,这个指令相当于pop+jump
,它先将数据(返回地址)弹出栈并保存到eip
中,然后处理器根据这个地址无条件地跳转到相应位置获取新的指令。