注意:本片只考虑局部变量,不考虑静态,全局变量等
困惑:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机值?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用是结束后怎么返回的?
注意:函数栈帧的创建和销毁在不同编译器下是略有差异的
函数栈帧理解前的铺垫:
寄存器:(集成到cpu中的,任何代码都可以使用)
-
EAX (Extended Accumulator):扩展累加器,用于算术和逻辑运算。在32位模式下,它与8位的
AL
和16位的AX
寄存器相关联。 -
EBX (Extended Base):扩展基址寄存器,通常用于存储数组索引或内存地址。
-
ECX (Extended Counter):扩展计数寄存器,常用于循环计数。
-
EDX (Extended Data):扩展数据寄存器,用于存储数据,特别是在输入/输出操作中。
-
EBP (Extended Base Pointer):扩展基址指针,通常用作栈帧的基址指针,用于访问局部变量和函数参数。
-
ESP (Extended Stack Pointer):扩展栈指针,指向当前栈顶的地址。
函数栈帧:
EBP,ESP这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
每一个函数调用都需要在栈区创建一个空间
下面,我们写一个足够简单的代码来观察:
#include<stdio.h>
int Add(int a, int b)
{
return a + b;
}
//精华讲解改为:
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
ebp,esp寄存器的作用:维护调用的函数的函数栈帧:
我们可以使用调试窗口中的调用堆栈观察函数调用的顺序:
其实在VSmain函数也是由别的函数调用的:(main函数的调用关系)
因为函数在栈上是要分配空间的,也就是说:
函数栈帧的创建:
通过以上的简单理解,我们可以通过反汇编来更好的理解函数栈帧的创建:
在调用main函数之前,调用的函数是_mainCRTStartup,既然进入到main函数了,_mainCRTStartup和mainCRTStartup的函数栈帧就已经创建好了,那么,_mainCRTStartup就应该有ebp,esp指针来维护:
进入main函数后的第一步是push,也就是压栈操作:
push:ebp
:后的变化:(push操作后,esp的指向位置会发生改变)
mov:ebp,esp
:接下来是mov指令,mov指令是把esp的值给ebp,也就是ebp存放的指针就不再指向原来的位置,而是指向现在esp指向的位置:
sub:esp,0E4h
:sub其实就是减操作,就是将esp减去0E4h(十六进制数字):也就是原来的esp变小了,不再指向原来位置:esp和ebp也就维护了一段新的空间,这是为main函数预开辟好的空间:
后面的三个push:也就是压栈3个元素:
lea指令也叫load effective address(加载有效地址) ,也就是把edi后面的地址加载到edi,简单来说,就是给edi一个地址:其实给edi的地址是【ebp-0E4h】,也就是存放指向原来esp(上图中打岔)位置:
然后又mov了ecx,eax,将其对应的值给到了本身:
接着,rep stos:dword ptr es:[edi](重要操作):要把从刚才的edi位置开始,向‘下’的ecx,也就是39h这么多个dword的数据全部改成eax的内容(0CCCCCCCCh),注意:一个word是2个字节,一个dword是4个字节:(每次初始化4个字节--->0CCCCCCCCh)
到这一步,已经是为main函数的开辟准备好了,接下来就是要执行正式有效的代码了:
int a = 10;
对应的汇编代码就是:
dward ptr [a],0Ah
去掉符号名:
dward ptr [ebp-8],0Ah
就是把0Ah这个十六进制数字放到ebp-8指向的位置上:
剩下的两条指令也是如此:
这是接下来的汇编代码:
mov eax,dword ptr [ebp-14h]
注意:这里的ebp-14h便是上面b的位置,这条指令的意思就是把【ebp-14h】的值放到eax里边去,也就是把b的值(20)放到eax里边去了,接着:
push eax
也就是压栈,把eax放到栈顶,push操作伴随着esp的位置改变:
接下来的指令:
mov ecx,dword ptr [ebp-8]
push ecx
也是同理:将a的值(1下划线0)放到ecx里边去:
那么,这两个动作是不是在传参呢?答案是确实是在传参
可以看出:传参是从右向左传参的
接下来的指令:
call 00C210E1
call指令就是要调去用函数区了,在此之前,我们应该注意这条指令对应的地址:
00C2144B
接下来,在调试(F11)下,就是执行call指令:
注意红色区域:call就把他的下一个地址add对应的地址压栈了,也就是:
这样的目的是为了call完后,程序能回到对应的位置,而压栈了call指令的下一条指令的地址,就可以保证,程序能够回到正确位置,接着调试,就进入到真正的Add函数里了:
这些指令也和main函数栈帧的创建思路一致:
push ebp
本来维护main函数的ebp进入到了压栈操作:为维护Add函数准备:
以下操作省略:
接下来执行计算操作:我们的x,y其实就是:
也就是形参是实参的一份临时拷贝--->改变形参不会影响实参
接下来的操作:将值返回:
return z
mov eax,dward ptr [ebp - 8]
return z的意思就是把ebp-8位置的值先给eax,不然z这个值出了Add函数就要销毁了
函数栈帧的销毁:
紧接着上述操作:要进行Add函数栈帧的销毁了
pop edi
就是将栈顶元素弹出,弹出到edi(edi弹出到edi),esp++
以下2个pop同理 :
接下来:
mov esp,ebp
将ebp的值赋值给esp,也就是esp指向ebp的位置,原本的空间得到释放回收:
接下来的pop指令,就是要pop掉栈顶:ebp->main,这就指令就可以使ebp退回指向原来维护的main函数位置,pop带着esp++ :
接下来的ret指令就是跳出Add到main函数栈帧了,这里靠的是call指令想在放在栈顶的地址,也就是说,存这个地址就是为了调用完函数,程序还可以回到原来位置
接下来的add指令:
add esp,8
就是esp+8:esp位置变化:
形参空间也就是被销毁了
接下来就把eax的值给了c
本篇逐步演示了栈帧的创建和销毁,感谢观看