在了解函数栈帧之前先要了解一些基础概念:
- 首先是程序在编译阶段发生的处理 参考一下博客:程序环境和预处理
总结一下就是 编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有特定的内存地址。在生成可执行程序时程序已经变成了供机器识别的二进制代码,二进制代码晦涩难懂,所以我们采用观察汇编语言来窥探函数栈帧的底层原理。 - 还有就是要了解一下内存的分区:
我们今天讨论的函数栈帧主要是在栈区上进行的。而栈区的结构是从高地址到低地址开辟空间
-
对一些汇编的基本符号的了解:
- 基本的寄存器:
esp: 始终指向栈顶空间
ebp: 是在堆栈中录地址用的,始终指向系统栈最上面一个栈帧的底部
eax/ebx/ecx/edx: 这些寄存器类似一个容器用啦储存一些数值
- 基本的寄存器:
-
对一些汇编的指令的了解:
1.mov esp,ebp
这里的mov 的意思是将ebp 的值赋值给 esp ——也就是把右边的值赋值给左边
2.pop edi
这里是把栈顶元素先出栈,再赋值给寄存器edi
3.push ecx
这里是把ecx的值压入栈顶
4.dword ptr [地址]
就是 该地址所指向的四字节数据、
5.rep stos dword ptr es:[edi]
的意思是覆盖从edi开始的内存为eax,edi减小4;循环ecx次。
6.sub esp,0E4h
这里的意思是将esp减去0E4h(16进制)
我们选取一个比较简单的函数调用来从汇编层面观察函数再栈区上面的创建:
#include<stdio.h>
int add(int x, int y)
{
return x+y;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = add(a, b);
return 0;
}
调试函数,转到反汇编
这就是add函数的汇编代码,下面来逐步解读:
– ❤️
- 第一步: 这里先将ebp压入栈,这时因为esp始终是指向栈顶的所以esp也指向ebp,
- 第二步: 将esp的值赋值给ebp,所以ebp也指向了栈顶了
– ❤️
- 第一步:将esp减去
0E4h
,也就是向上位移了0E4h
- 第二三四步:向栈里压入了ebx、esi、edi三个值,每压一个值esp都要向上移动,因为esp始终指向栈顶
- 最后一步: ebp-0E4h的值存入edi中
– ❤️
- 第一步: 将39h(也就是十进制的57)存入寄存器ecx
- 第二步: 将
0CCCCCCCCh
存入寄存器eax - 第三步: 文章上面也介绍过了这个语句的用法:
rep stos dword ptr es:[edi]
的意思是覆盖从edi开始的内存为eax,edi增加4;循环ecx次。
因为edi存的内存地址是ebp-0E4h
,而0E4h
的十进制的值为 228 ,循环次数ecx 39h
是十进制的57。然而228是57 的四倍,所以这个指令的含义就是将ebp
到 ebp-0E4h
的每四个字节的空间都拷贝成eax的值也就是0CCCCCCCCh
这里用来填充中间没存的cc cc cc cc
实际上就是我们有时访问内存越界的时候看到的 烫烫烫烫烫烫烫烫烫烫烫烫…
– ❤️
- 第一步:向
ebp-8
的位置赋值0Ah,十进制里的10 也就是a的值 - 第二步:向
ebp-14h
的位置赋值14h 十进制里的20,也就是b的值 - 第三步:向
ebp-8
的位置赋值0 也就是c的值
– ❤️
- 第一步 :前两行是首先把
ebp-14h
的值赋给寄存器eax,并把eax压入栈 - 第二步:后两行是首先把
ebp-8h
的值赋给寄存器ecx,并把ecx压入栈
这里其实就是所谓的函数传参,将函数传给一个创建的临时变量:
– ❤️
- 这里就是调用add函数,call是指访问add所在的内存的地址,但是这里要注意的一点是,在访问add所在函数的地址的时候,编译器将call指令的下一条指令压入栈中(记住这里后面会很有用)
按f11访问函数内部
跳转到函数的代码的汇编
接下来就要访问add函数内部了!
– ❤️
由于创建该函数的栈帧与创建main函数时的一模一样,所以这里就省略了创建函数栈帧的具体过程
这里寄存器 esp 和 ebp之间所夹的区域为add的函数栈帧
– ❤️
- 第一步: 将
ebp+8
所指向的值,(也就是传进来的形参10) 存入寄存器eax中 - 第二步:将
ebp-0Ch
所指向的值,(也就是传进来的形参20) 与寄存器eax相加,这时寄存器eax存储的就是30
– ❤️
- 第一步: 这里分别将寄存器edi esi ebx 出栈
- 第二步:将esp的地址加上
0C0h
,也就是在初始化的时候esp减去的大小,所以这时esp和ebp指向同一块空间
– ❤️
- 第一步:将ebp的值赋给esp
- 第二部 :将栈顶的元素弹出,并赋给ebp,注意:这里栈顶存的ebp实际上是main函数栈帧ebp所指向的ebp的地址
– ❤️
- 这时进行到了return ,也就是add函数执行完成,返回main函数
但是返回main函数并不知道从哪个指令开始执行,所以这里就体现了—将call下一条指令存入函数栈帧的作用,使得调用函数结束之后依然能找到下一条指令,事实也是如此:
– ❤️
- 第一步:将esp的地址加上8,由于每个都是四字节,也就是向下移动两位
- 第二步:将eax也就是我们在add里得到的返回值,赋值给
ebp-20h
也就是c所在的地址空间
第二步将返回值成功的带回了主函数!
– ❤️
后面对于main函数的销毁和 对于add的销毁是一模一样的 ,这里就不在赘述了
总结:
函数栈帧让我们充分的意识到函数建立的过程包括:
- 函数的形参是如何传递进函数:在函数栈帧上拷贝给一个临时变量
- 返回值是如何传递回主函数的:由一个寄存器储存带回主函数,与形参 和 函数内创建的临时变量无关(出函数就会被销毁)