[C语言]函数栈帧的创建和销毁(图文详解)

文章目录

  • 函数栈帧定义
    • 寄存器
  • 函数栈帧的创建
    • main函数栈帧的创建
    • Add函数栈帧的创建
    • Add函数栈帧的销毁
  • 总结

函数栈帧定义

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。


寄存器

寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。

寄存器种类

eax ebx ecx edx
ebp esp …

其中ebp和esp这两个寄存器是用来存放维护函数栈帧的地址。

函数栈帧的创建

每一个函数调用,都要在栈区创建一个空间。

  • 注:栈区的存储是由高地址向低地址存储的。

测试代码:

#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\n", c);

	return 0;
}

在vs2013的环境下,main函数也是被其他函数调用的。

[C语言]函数栈帧的创建和销毁(图文详解)_第1张图片

实质上main函数的调用过程是:
[C语言]函数栈帧的创建和销毁(图文详解)_第2张图片
大致轮廓是:
[C语言]函数栈帧的创建和销毁(图文详解)_第3张图片
反汇编代码

     1: #include <stdio.h>
     2: int Add(int x, int y)
     3: {
008F13C0  push        ebp  
008F13C1  mov         ebp,esp  
008F13C3  sub         esp,0CCh  
008F13C9  push        ebx  
008F13CA  push        esi  
008F13CB  push        edi  
008F13CC  lea         edi,[ebp+FFFFFF34h]  
008F13D2  mov         ecx,33h  
008F13D7  mov         eax,0CCCCCCCCh  
008F13DC  rep stos    dword ptr es:[edi]  
     4: 	int z = 0;
008F13DE  mov         dword ptr [ebp-8],0  
     5: 	z = x + y;
008F13E5  mov         eax,dword ptr [ebp+8]  
008F13E8  add         eax,dword ptr [ebp+0Ch]  
008F13EB  mov         dword ptr [ebp-8],eax  
     6: 	return z;
008F13EE  mov         eax,dword ptr [ebp-8]  
     7: }
008F13F1  pop         edi  
008F13F2  pop         esi  
008F13F3  pop         ebx  
008F13F4  mov         esp,ebp  
008F13F6  pop         ebp  
008F13F7  ret  
     8: 
     9: int main()
    10: {
008F1410  push        ebp  
008F1411  mov         ebp,esp  
008F1413  sub         esp,0E4h  
008F1419  push        ebx  
008F141A  push        esi  
008F141B  push        edi  
008F141C  lea         edi,[ebp+FFFFFF1Ch]  
008F1422  mov         ecx,39h  
008F1427  mov         eax,0CCCCCCCCh  
008F142C  rep stos    dword ptr es:[edi]  
    11: 	int a = 10;
008F142E  mov         dword ptr [ebp-8],0Ah  
    12: 	int b = 20;
008F1435  mov         dword ptr [ebp-14h],14h  
    13: 	int c = 0;
008F143C  mov         dword ptr [ebp-20h],0  
    14: 	c = Add(a,b);
008F1443  mov         eax,dword ptr [ebp-14h]  
008F1446  push        eax  
008F1447  mov         ecx,dword ptr [ebp-8]  
008F144A  push        ecx  
008F144B  call        008F10E1  
008F1450  add         esp,8  
008F1453  mov         dword ptr [ebp-20h],eax  
    15: 	printf("%d\n", c);
008F1456  mov         esi,esp  
008F1458  mov         eax,dword ptr [ebp-20h]  
008F145B  push        eax  
008F145C  push        8F5858h  
008F1461  call        dword ptr ds:[008F9114h]  
008F1467  add         esp,8  
008F146A  cmp         esi,esp  
008F146C  call        008F113B  
    16: 
    17: 	return 0;
008F1471  xor         eax,eax  
    18: }
008F1473  pop         edi  
008F1474  pop         esi  
008F1475  pop         ebx  
    18: }
008F1476  add         esp,0E4h  
008F147C  cmp         ebp,esp  
008F147E  call        008F113B  
008F1483  mov         esp,ebp  
008F1485  pop         ebp  
008F1486  ret  

初始状态:
[C语言]函数栈帧的创建和销毁(图文详解)_第4张图片

main函数栈帧的创建

第一步进行push指令,将ebp压入栈顶
[C语言]函数栈帧的创建和销毁(图文详解)_第5张图片
将ebp压入栈,esp的指向也随之发生改变。
根据内存和监视可以看出ebp确实被压进了栈区。
[C语言]函数栈帧的创建和销毁(图文详解)_第6张图片
[C语言]函数栈帧的创建和销毁(图文详解)_第7张图片

第二步进行move指令,将esp的值给ebp
[C语言]函数栈帧的创建和销毁(图文详解)_第8张图片
通过观察内存:
[C语言]函数栈帧的创建和销毁(图文详解)_第9张图片
第三步进行sub指令,将esp指向的地址减去0E4h(十进制228)。
[C语言]函数栈帧的创建和销毁(图文详解)_第10张图片
随后分别把ebx,esi,edi压入栈顶
[C语言]函数栈帧的创建和销毁(图文详解)_第11张图片
随后四句指令,把从edi(ebp-0E4h)开始的ecx(39h)个 空间改成eax(0CCCCCCCCh)。这么做是为了给main函数栈帧初始化。
[C语言]函数栈帧的创建和销毁(图文详解)_第12张图片
随后对变量a,b,c 进行创建和初始化。
[C语言]函数栈帧的创建和销毁(图文详解)_第13张图片

Add函数栈帧的创建

当abc变量创建好了之后,就要开始调用add函数。
随后分别将ebx(20)和ecx(10)压入栈顶。
这两个指令实际上是在为Add函数传参。
[C语言]函数栈帧的创建和销毁(图文详解)_第14张图片
随后call指令是调用函数指令,它会把call指令的下一条指令的地址压入栈顶。这样做的目的是等Add函数调用结束后,就会回到call指令的下一条指令继续执行。
[C语言]函数栈帧的创建和销毁(图文详解)_第15张图片

进入Add函数后,前面的几条指令跟进入main之前的几条指令一样,是为了给函数准备栈帧和对其进行初始化。
[C语言]函数栈帧的创建和销毁(图文详解)_第16张图片

随后在ebp-8的空间创建临时变量z并初始化为0,再通过ebp+8和
ebp+0Ch找到main函数中传递的a,b参数作为形参x,y,相加得到的值赋给eax,再由eax把值赋给z。

可以得出结论:形参是实参的一份临时拷贝。
随后把z的值放到eax寄存器里面,当函数调用结束后z变量销毁后回到主函数可以通过eax找到计算后的值。
[C语言]函数栈帧的创建和销毁(图文详解)_第17张图片

Add函数栈帧的销毁

随后三句pop指令分别把edi,esi,ebx弹出栈区,把ebp的值给esp,再弹出之前main函数的ebp地址给ebp,Add栈帧销毁,随后回到主函数执行接下来指令。
[C语言]函数栈帧的创建和销毁(图文详解)_第18张图片
随后ret指令找到之前已经压入栈的call指令的下一条指令的地址 ,就回到主函数的call指令的下一条指令。
再将esp指向的地址加8,把形参的内存释放。
在通过ebp-20h找到main函数的c变量,然后再把eax的值赋给c,实现了把Add函数计算结果带回到main。
[C语言]函数栈帧的创建和销毁(图文详解)_第19张图片

总结

  • 局部变量是怎么创建的?

先给函数栈帧分配空间并初始化,在创建局部变量在栈帧里并赋值。

  • 为什么局部变量的值是随机值?

因为如果局部变量不初始化,函数栈帧里面的值全是0xcccccccch,所以局部变量的值是随机值。

  • 函数是怎么传参的?传参是顺序的怎样的?

当函数调用之前,会先把参数从右向左压入栈区,随后进入函数通过指针的偏移量来找到形参。

  • 形参和实参是什么关系?

形参和实参只是值相同,它们的空间是独立的,所以说形参是实参的一份临时拷贝。

  • 函数调用结束后是怎么返回的?

函数在调用时会先把call指令的下一条指令的地址和调用上一个函数的ebp压入栈,当函数结束调用时弹出值就可以跳转到上一个函数的栈帧空间。

你可能感兴趣的:(指针,堆栈,c++,c语言,栈)