亲们,今天讲讲函数栈帧的创建与销毁。这里有几个问题,如果你能不加思索的回答出来。
那我只能说,大佬请去别的地方一展拳脚吧,当然你能指出我的不足。我也会深表感激的。
这几个问题如果你能在看完我的文章后搞明白,那么说明我还是讲的不错的撒。
目录
问题
1. 基础知识
2. 以vs2013演示全过程
2.1 观察函数间的调用
2.2 函数栈帧的创建
2.3 局部变量的创建
2.4 函数的调用
2.4.1实参的拷贝
2.4.2 Add函数的调用
2.4.3 Add函数的返回
1. 局部变量是如何创建的?
2. 为什么局部变量不初始化内容是随机的?
3. 函数调用时参数时如何传递的?
4. 传参的顺序是怎样的?
5. 函数的形参和实参分别是怎样实例化的?
6. 函数的返回值是如何带回的?
函数在调用的时候都会在内存中的栈区上开辟一份空间供函数使用(并非数据结构中的栈哈),我们想要解决如上问题就需要理解函数栈帧的创建和销毁。
我们都知道在CPU中有许多的寄存器(相比内存中的存储单元个数少太多了,不能说很多哈),比如:eax,ebx,ecx,edx······这些寄存器不是咱这片文章的重点。其中有这么两个寄存器:ebp和esp,它们里面存放的是地址,这两个地址是用来维护函数栈帧的。
比如说,当前程序正在main函数里,并未跳转到其他函数。那么ebp和esp这两个地址之间的存储单元就是main函数的栈帧,ebp和esp负责维护它之间的函数栈帧。ebp指向的元素称为栈底元素,
esp指向的元素称为栈顶元素。
上图:
还有一点:内存上的栈区先使用高地址,再使用低地址 。
我们先看如下程序:
#include
//求和函数,参数x,y表示两个加数
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);
return 0;
}
大家肯定都会说,这很简单嘛,不就是main函数调用了Add函数嘛,是的没错。但是通过vs2013的调试----调用堆栈我能看到更多信息(该功能调试时可以用来显示函数之间的调用逻辑,工程庞大时很好用):
我们看到实际上是:mainCRTStartup()这个函数调用了_tmainCRTStratup()函数,然后是 _tmainCRTStratup()函数调用main函数,最后是main函数调用Add函数。是不是有多了一点奇怪的知识呢?
上面说到每个函数都有自己的栈帧,上面那两个名字忒长的函数也不例外。咱就来讲讲main函数栈帧的创建。要想理解透,咱就得分析汇编代码。上图:
函数栈帧的创建与ebpesp这两个寄存器紧密相关,咱要把它监视起来。
下面我们一步一步分析汇编代码。
1):push ebp 代表将ebp的地址压入栈区,同时esp指向的位置减一:
我们通过内存地址的变化也能看到esp指向位置的变化,以及压入的内容:
2):mov ebp, esp 代表将esp的值给ebp,那么他们指向的位置就相同啦:
3):sub esp,0E4h 代表将esp的地址减去0E4h这样一个16进制数(h代表十六进制嘛)
4),5),6):push ebx , push esi , push edi 同1)压入这三个寄存器的地址 。压入完成esp的地址减 3 (-1代表一个减去四个字节哈,32位的指针大小是4字节嘛)。
7):lea edi , [ ebp - 0E4h ] : lea是load effective address 即加载有效地址的意思,表示将
ebp-0E4h这个地址加载到edi里面去。这个地址就是ebx下面的那个地址,上图看ebx的位置,第3)部解释为什么是ebx下面的那个位置。
8):mov ecx ,39h 表示将39h给到ecx里面。
9):mov eax ,0CCCCCCCCh 表示将0CCCCCCCCh 给到eax里面去。
10):rep stos dword ptr es:[edi] 表示从edi这个位置开始向下39h个 dword(double word 双字,即四字节)的数据改为CCCCCCCC。(刚刚好到ebp上面的那个位置嘛,0E4h代表十进制的228, 39h代表十进制的57, 57 * 4字节正好是228,就刚刚好到ebp上面的位置嘛)。
1): mov dword ptr [ebp-8] , 0Ah 表示将0Ah(就是十进制的10,即a的值)放到dword(四字节)的指针(int) [ebp-8] 里面去 。
2): mov dword ptr [ebp-14h],14h 同上。
3):mov dword ptr [ebp-20h] , 0 同上。
到这里大家是不是十分明白为什不能用未初始化的局部变量了撒。因为是随机值嘛。
注意:不同编译器每个变量开辟的空间的间隔可能是不相同的。
1):mov eax , dword ptr [ebp-14h] 把 [ebp-14h] 里面的内容给到eax,[ebp-14h] 里面的内容就是b的值呀。
2):push eax 压栈,压入eax 。
3):mov ecx,dword ptr [ebp-8] ,同上将b的值给到ecx。
4):push ecx 压栈,压入ecx 。
进行一次push esp地址减1, 1),2),3),4)结束后esp减了2(这个 - 2代表减去八个字节):
5):call 003C10E1 表示调用Add函数,并且把call指令下一条指令的地址压栈。为什么呢?因为call指令表示调用Add函数,调用函数完了,肯定得继续执行call指令下面的指令撒,调用call的时候记住下一条指令的地址,调用完了才回得来嘛。举个不恰当的比喻:你去朋友家里玩,玩完了,肯定的回自己的家撒,如果你去的时候都记不住你家的地址了,等你玩完了,还怎么回来嘞。
进入Add函数后我们看到它的汇编代码的前面和main函数创建栈帧的时候是差不多的。简单分析一下即可。也可以来检验一下亲们,刚刚main函数栈帧的创建搞懂没有哦。画一下图呗!
1):push ebp 压栈,压入ebp的值(存的是地址嘛)
2):mov ebp , esp 将esp里面的值给到ebp。
3):sub esp , 0CCh esp - 0CCh 改变esp指向的位置。
4):push ebx 压栈,压入ebx的值。
4):push esi 压栈,压入esi的值。
5):push edi 压栈,压入edi的值。
6):lea edi , [ ebp-0CCh ] 加载有效地址,将ebp - 0CCh的地址给到edi 。
7):mov ecx,33h 将33h的值给到ecx。
8):mov eax,0CCCCCCCCh 将0CCCCCCCCh的值给到eax里面。
9):rep stos dword ptr es:[edi] 表示从edi这个位置开始向下33h个 dword(double word 双字,即四字节)的数据改为CCCCCCCC。(刚刚好到ebp上面的那个位置嘛。原因上面创建main函数栈帧讲过了)
10):mov dword ptr [ebp-8] , 0 将0,给到ebp-8指向的位置。就是给局部变量z开辟了空间。
11):mov eax , dword ptr [ebp+8] 把ebp + 8 里面的值放到eax里面去。
12):add eax , dword ptr [ebp+0Ch] 把ebp+0Ch(ebp+12)里面的值加到eax里面去。
到这里,eax的值变成了30.(你可能会想为什么 11)不会把下图中eax--20 的值覆盖呢?eax是一个在CPU里面的寄存器,这里的eax--20只是说明eax 上次 eax用了过后,里面存留的是20 ,11)步是将eax的值改成了10,上次的push eax是将eax的值压入了内存的栈区 )
13):mov dword ptr [ebp-8] , eax 将eax里面的值放到ebp - 8 的位置里面去。
到此我们可以看出形参并没有创建,而是在 调用Add函数前,拷贝了一份实参,压入到内存的栈区,调用Add函数再找到这个地址。由此可以得出结论:形参是实参的临时拷贝。所以改变形参不会影响实参。
我们还可以看出b的值20,是先压栈的,故传参的时候是先传b的。
1):mov eax,dword ptr [ebp-8] 把ebp - 8 的值给到eax里面,即是z的值30。这样就能解释为什么z销毁了还能返回z的值。
2): pop edi 弹栈, 弹出一个栈顶元素,将弹出的值放到edi里面去,esp加一。
3): pop esi 弹栈, 弹出一个栈顶元素,将弹出的值放到esi里面去,esp加一。
4): pop ebx 弹栈, 弹出一个栈顶元素,将弹出的值放到ebx里面去,esp加一。
5):mov esp , ebp 将ebp的值给到 esp。执行完这条指令,esp, esp指向同一个地方。即存放原来main函数的ebp的位置。
6):pop ebp 弹出一个栈顶元素,将弹出的值给到ebp里面去,这个栈顶元素就是,存放原来main函数的ebp的位置。这么搞完又回到了main的栈帧了。
7) ret 弹出栈顶元素(call指令下一条指令地址),并跳到那个地址处。这就是存call指令下一条指令地址的原因。跳到call下一条地址:
8):add esp,8 把esp存的地址加8。esp指向了图中edi的位置(ret弹出的时候加了1个字节),此时形参销毁,注意形参销毁的时间。
9):mov dword ptr [ebp-20h],eax 将eax中的值给到 ebp -20h中去 , 看上图,就是为c开辟的那一块空间。即把求得的和给到c里面去。
好啦,今天的函数栈帧的创建与销毁就到这,看完了可以看看前面的问题,看能否回答上,以检验学习成果。加油,亲们。