我们都知道我们的存储区域分为栈区、堆区和静态区。
每一次函数的调用都要在栈区上分配一块空间以保存信息,这就是函数栈帧,也叫函数的调用堆栈。
在了解栈帧的创建和销毁之前,我们还得了解一些东西:
1. 寄存器:寄存器分为:eax,ebx,ecx,edx,ebp,esp
其中ebp,esp存放的是地址,主要是用来维护栈帧的。
2.在一些编译器中,main函数也是被其他函数调用的
铺垫完成了,接下来进入正题:
先来一段简单的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = add(a, b);
return 0;
}
我们先看看这段代码创建栈帧的整体情况是怎样的:
接下来我们来看看栈帧创建的具体细节
先进行以下操作
1.按下Fn+F10
2.右击鼠标,点击转到反汇编
这是就来到了C语言对应的汇编代码
3.再次右击鼠标,点击显示符号名,将勾去掉
变成以下界面
下面我们就看看栈帧究竟是如何创建的:
我们在前面已经说过了,main函数也是被__tmainCRTStartup调用的,所以在创建mian函数的栈帧时,栈区的情况应该如下(注意:栈帧创建都是在栈区中进行的):
我们看看上面反汇编语句的第1句和 第2句
1. push ebp ——将ebp压入栈中(esp的指向自动改变)
2. mov ebp,esp ——将esp的地址赋给ebp
3.sub esp,0E4h ——将esp的地址减去0E4h个字节(即十进制的228),这是在为main函数创建栈帧 开辟空间
4.push ebx——将ebx压入栈中
5.push esi——将esi压入栈中
6.push edi——将edi压入栈中
(这3个操作具体是做什么用的现在我们不用管)
7.lea edi,[ebp+FFFFFF1Ch],此时勾上显示符号名,变为
lea edi,[ebp-0E4h]——lea:加载有效地址,将[ebp-0E4h]赋给edi (0E4h前面出现过)
以下3句合着一起看,说人话就是将edi指向的地址以下39h个dword个字节的内容改为eax中的内容
8.mov ecx,39h ——将39h(即十进制的57)赋给ecx
9.mov eax,0CCCCCCCCh ——将0CCCCCCCCh赋给eax(不同编译器赋的值不一样)
10. ret stos dword ptr es:[edi] ——一个word是两个字节,dword就是4个字节
57*4=228
以上相当于初始化main函数开辟的空间,这也是为什么我们有时会打印出“烫烫烫”
11.move dword ptr[ebp-8],0Ah ——将0Ah(十进制的10)放到由ebp-8开始的dword个字节中, 这是为局部变量a=10分配空间
12.move dword ptr[ebp-14h], 14h
13.move dword ptr[ebp-20h], 0
这两句话和上面的一样,分别是为b,c分配空间并初始化,从中我们也可以看出为它们分配的空间是不连续的。
14,15和16,17是两组一样的操作,其实是进行传参的操作
14. mov eax ,dword ptr [ebp-14] ——将从地址ebp-14开始的dword个字节的内容放到eax中
15 push eax ——将eax中的内容压栈
16. mov eax ,dword ptr [ebp-8] ——将从地址ebp-8开始的dword个字节的内容放到eax中
17 push eax ——将eax中的内容压栈
18.call 00C210E1 ——调用函数指令,执行该语句会将call指令下一句语句的地址(即第19句的地址)压入栈中,因为调用函数要跳转到函数内部去,当函数调用结束时还要找回来执行下一句语句
按Fn+F11进入函数内部
18.1到18.11的指令与前面main函数创建栈帧的前几个指令一样,这里是为add函数创建栈帧,然后为z分配空间
18.12 mov eax ,dword ptr [ebp+8] ——将从地址ebp+8开始的dword个字节的内容放到 eax 中,即将形参x的值放到eax中,此时eax=10
18.13 add eax ,dword ptr [ebp+0Ch] ——从地址ebp+0Ch开始的dword个字节里的内容(即y 的值)与eax相加并放到eax中,此时eax=30
18.14 mov dword ptr [ebp-8],eax ——将eax放到从地址ebp+8开始的dword个字节中,即 为z分配的空间内,此时z=30
18.15 mov eax dword ptr [ebp-8]——将从地址ebp88开始的dword个字节的内容放到 eax 中,即将z的值放到eax中,此时eax=30
因为此时函数调用要结束了,且要返回z,但z在栈帧销毁后就不存在了,所以要将z的内 容放到寄存器eax中保存好
此时add函数调用结束,开始add函数栈帧的销毁
18.16 pop edi ——弹出一个栈顶元素放到edi中
18.17 pop esi ——弹出一个栈顶元素放到edi中
18.18 pop ebx ——弹出一个栈顶元素放到edi中
18.16,18.17,18.18是一样的操作,分别将edi,esi,ebx出栈,然后分别放到edi,esi,ebx 中,即销毁
18.19 move esp,ebp ——将ebp的值赋给esp,即继续销毁栈帧
18.20 pop ebp ——弹出一个栈顶元素放到ebp中,即ebp获得ebp-main的值
18.21 ret ——作用是弹出栈顶元素的地址,即call下一条指令的地址,然后跳转过去,此时又回 到以下界面,并开始执行第19句语句
栈帧情况为:
19.将esp指向的地址加8给字节(即销毁形参x和y)
20.mov dword ptr [ebp-20h],eax ——将eax的内容放到从地址ebp-20h开始的dword个字节的 中,即将eax的值放到c中,此时c=30
后面main函数栈帧的销毁也是一样的,这里就不在过多赘述
下面是前面内容的整合,从最后一张图后面往前面看