在讲解函数栈帧前,我们需要先了解一些知识
首先我们得知道函数栈帧是在栈上开辟的
栈的特点是先使用高地址,后使用低地址
每一个函数调用都要在栈区创建一个空间
main函数也是被其他函数调用的,那两个函数又是被操作系统调用的,那两个函数就不多提了。
在了解函数栈帧前,我们还得再简单的了解一些寄存器和汇编命令
寄存器:
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip: 指令寄存器,保存当前指令的下一条指令的地址
汇编指令:
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器发生改变,移动到新的栈顶
pop:数据弹出至指定位置,同时esp栈顶寄存器也发生改变
sub:减法指令
add:加法指令
call:函数调用,1、压入返回值地址;2、转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似(pop eip)指令
下面我们就开始吧!
首先我们先写一段代码,之后的分析是根据这段代码展开的
int My_Add(int a,int b)
{
int c=0;
c=a+b;
return c;
}
int main()
{
int x=10;
int y=20;
int z=0;
z=My_Add(x,y);
printf("%d\n",z);
}
main这一块的函数栈帧由esp栈顶寄存器和ebp栈底寄存器维护;
然后将变量x,y,z分别压入main函数栈帧,我们发现是先使用地高地址,后使用低地址。而且我们还发现,变量之间没有紧挨着,而是空了一定的空间,我们目前先理解成,为了安全。
现在要开始调用函数了
我们发现在函数调用前,做了一个重要的事:
1、形成临时拷贝,也就是说:临时变量的形成是在函数正式被调用之前形成的
2、且形参的拷贝是从又右向左的
esp移动到了新的栈顶
下一步就到了很关键的call指令了
call函数调用:压入返回值地址,这个地址是call命令的下一条指令的地址;
jump通过修改eip,让eip保存0x777地址,eip保存的地址就是下一条指令的地址,转入目标函数,进行调用,0x777就是My_Add函数的地址。
为什么call要压入返回值?
函数是要调用完毕的,调用完毕得回来吧,回来就要有返回值,继续后面的语句,保存call的下一条地址,一会儿你就看到它的作用了
现在要进入My_Add函数啦!
干了几个事
1、压入main栈底地址(至于为什么后面瞧)
2、mov ebp esp
现在esp在新栈顶,mov esp的地址到ebp中,现在esp和ebp指向同一个地址(main栈底地址)
现在要形成My_Add的函数栈帧了
这里首先
sub esp cch
意思是将esp寄存器减去一个值,这个值的大小是由编译器根据你所调用函数的大小决定的,现在esp到新的栈顶,esp和ebp这之间的空间,就是新维护的My_Add函数栈帧,至此我们就进入My_Add函数了。
然后先压入c变量的栈帧
下面进行计算了
根据ebp+某值找到实参临时拷贝的那两个地方,进行计算,将值赋于c
这段汇编是这样的
mov eax [ebp+8] //[ebp+8]就是拷贝的10的地址
add eax [ebp+12] //[ebp+12]就是拷贝的20的地址
mov [ebp-8] eax //[ebp-8]就是c的地址
由此我们计算出来两数的和了
下面我们要开始return了
在return之前我们需要做一件重要的事情
mov eax [ebp-8]
这句汇编指令意思是将c的值保存到一个eax的寄存器中(保存返回值),之后会用到
然后我们mov esp ebp
将ebp的值放入到esp意思就是现在ebp和esp又回到同样的地方(都指向main栈底地址处),那么My_Add的栈帧也就被释放了。
下面我们继续返回
我们执行
pop ebp
意思就是弹栈,将main栈底地址给ebp,那么ebp现在就回到它最开始的地方了(这就解释了上面为什么要压入main栈底地址),esp也移动到新的栈顶处(0x123返回值处)。
下面我们要ret恢复返回值地址了
相当于要pop eip
就是把现在0x123的地址压入eip中,而eip寄存器保存的是下一条要执行的指令地址,而0x123就是call指令的下一条指令的地址,至此我们就返回到main函数中(也就解释了为什么要压入返回值地址)
esp也移动到新的栈顶如图
之后再
add esp 8
这句指令的地址就是call的下一条指令的地址(0x123)
意思是将esp地址+8移动到新的栈顶如图
至此我们看到,现在esp和ebp又重新维护main函数栈帧
最后一步
mov [ebp-20h] eax //[ebp-20h]就是z的地址
这一步就是将eax寄存器保存的返回值给到z,至此我们的函数栈帧就全部完毕!
总结:
1、调用函数前,需要先形成临时拷贝,形成过程是从右向左的
2、临时空间的开辟,是在对应函数栈帧内部开辟的
3、函数调用完毕,栈帧结构被释放掉
4、临时变量具有临时性的本质:栈帧具有临时性
5、调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
6、函数调用,因拷贝所形成的临时变量(push),变量于变量之间的位置关系具有一定的规律
就第6条看一个代码
int My_Add(int a,int b)
{
printf("before: %d\n",b);
*(&a+1)=100;
printf("after: %d\n",b);
}
因为在栈上拷贝形成的临时变量是push进去的,所以相对位置是有有规律的,是挨着的,所以根据a变量的地址可以找到b变量的地址,从而修改b。
所以现在的栈随机化技术就是为了防止某些黑客根据某些特定的位置推断出其他位置,从而进行访问等。
这期就到这里啦,感谢阅读,我们下期再见
如有错 欢迎提出一起交流
关注周周汪哦
关注三连么么么哒