带你领略函数栈帧创建和销毁的问题

我们在学习C语言的过程中,你会不会有这样的一种困惑?
局部变量是怎么创建的?
为什么局部变量的值是随机值?
函数是怎么传参的?传参的顺序是怎样的?
新参和实参是什么关系?
函数调用是怎么做的?
函数调用结束后怎么返回的?

其实这些问题都与函数栈帧的创建和销毁有关,今天我们就来探讨一下这里面的实质性内容(使用的是vs2019)。

在讲解函数栈帧之前,我们首先了解一下什么是寄存器。
寄存器: 它是我们电脑中央处理器的组成部分,寄存器主要存储的是二进制代码,它可以暂存指令、数据和地址。
既然讲到函数栈帧,那我们就了解一下这两个寄存器 ebpesp ,这两个寄存器存放的都是地址,而这两个地址是用来维护函数栈帧的。

一、理解反汇编

1、main函数的调用原理

main函数也需要开辟一段内存空间。其实main函数也又别的函数调用的。

mainCRTStarup调用__tmainCRTStarup,然后__tmainCRTStarup调用main函数

还没调用main函数的时候, __tmainCRTStarup内存空间是这样的,__tmainCRTStarup函数的栈帧由当前的 esp 和 ebp 维护。

我们都知道函数都是在栈中创建内存空间。
带你领略函数栈帧创建和销毁的问题_第1张图片

2、反汇编

我们来看一个程序

#include 
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);
	printf("%d", c);
}

我们首先F10调试代码,转到反汇编可以看到如下图所示的反汇编代码
带你领略函数栈帧创建和销毁的问题_第2张图片
我们一步步来理解每句汇编语言

一开始esp和ebp的地址为
在这里插入图片描述
004428D0 push ebp // 将ebp压栈
ebp成为这时的栈顶,这时esp就指向最新栈顶,由此可知,esp的地址会变小
带你领略函数栈帧创建和销毁的问题_第3张图片
004428D1 mov ebp,esp
esp的值给ebp
004428D3 sub esp,0E4h
esp-0E4h esp向后退,为mian函数留出一段空间。
下图为内存的布局图
带你领略函数栈帧创建和销毁的问题_第4张图片
004428D9 push ebx
004428DA push esi
004428DB push edi
将ebx,esi,edi压栈
004428DC lea edi,[ebp-24h]
lea – load effecitve address 负载的有效地址
将ebp-24h加载到edi中,相当于把edi赋个地址
004428DF mov ecx,9
把9放在ecx中
004428E4 mov eax,0CCCCCCCCh
把0CCCCCCCCh放在eax中
004428E9 rep stos dword ptr es:[edi]
将edi下面的ecx这么多个双字节数据全部赋值为eax(dword是双字节)
带你领略函数栈帧创建和销毁的问题_第5张图片

004428EB mov ecx,44C003h
将44C003h赋值给ecx
002028F0 call 00201320
call指令为调用
int a = 10;
004E28F5 mov dword ptr [ebp-8],0Ah
0Ah 为十六进制 也就是十进制的10 赋值给ebp-8
int b = 20;
004E28FC mov dword ptr [ebp-14h],14h
14h 为十六进制 也就是十进制的 20 赋值给ebp-14h
int c = 0;
004E2903 mov dword ptr [ebp-20h],0
0赋值给ebp-20h
c = Add(a, b);
004E290A mov eax,dword ptr [ebp-14h]
ebp-14h 也就是20 双字节赋值给eax
004E290D push eax
eax 压栈
004E290E mov ecx,dword ptr [ebp-8]
ebp-8 也就是10 双字节赋值给ecx
004E2911 push ecx
ecx压栈
带你领略函数栈帧创建和销毁的问题_第6张图片
004E2912 call 004E140B
调用函数 把下条指令的地址压栈 按F11进入函数的反汇编

同理下面函数的创建也是一样这时又在内存中创建空间
int Add(int x, int y)
{
00E018E0 push ebp
00E018E1 mov ebp,esp
00E018E3 sub esp,0CCh
00E018E9 push ebx
00E018EA push esi
00E018EB push edi
00E018EC lea edi,[ebp-0Ch]
00E018EF mov ecx,3
00E018F4 mov eax,0CCCCCCCCh
00E018F9 rep stos dword ptr es:[edi]
00E018FB mov ecx,0E0C003h
00E01900 call 00E01320
int z = 0;
00E01905 mov dword ptr [ebp-8],0
将0赋值给ebp-8
z = x + y;
00E0190C mov eax,dword ptr [ebp+8]
将ebp+8这个空间的值给eax
00E0190F add eax,dword ptr [ebp+0Ch]
将ebp+0Ch这个空间的值给加eax再赋值给eax
00E01912 mov dword ptr [ebp-8],eax
将eax这个值赋值给ebp-8
return z;
00E01915 mov eax,dword ptr [ebp-8]
把 ebp-8 也就是 z 的值放在eax中
}
00E01918 pop edi
00E01919 pop esi
00E0191A pop ebx
edi esi ebx 全部出栈
00E0191B add esp,0CCh
00E01921 cmp ebp,esp
00E01923 call 00E01244
00E01928 mov esp,ebp
ebp 赋给 esp
00E0192A pop ebp
ebp 出栈 这个时候栈顶就是上次call指令下一条指令的地址
00E0192B ret
回到main函数
004E2917 add esp,8
esp加8
004E291A mov dword ptr [ebp-20h],eax
将eax 赋值给ebp-20h 也就是将 z 的值放在main函数栈空间中的一个位置
printf("%d", c);

下图是mian函数栈帧的创建,Add函数的创建也是同理。
带你领略函数栈帧创建和销毁的问题_第7张图片

二、总结

函数栈帧的原理就是这样,基本上一开始的问题也都可以慢慢的解决。慢慢消化吧!加油!

你可能感兴趣的:(C语言,c语言)