C语言进阶——函数栈帧的创建和销毁

目录

1、函数栈帧相关概念     

2、函数栈帧的创建与销毁

2.1main函数的创建

2.2main函数中变量的创建

2.3 Add函数栈帧的创建

2.4 Add函数栈帧的销毁

在C语言的学习中你可能有很多问题:

        局部变量是怎么创建的?为什么局部变量的值是随机的?函数是怎么传参的?传参的顺序是怎样的?形参和实参的关系是什么?函数是如何被调用的?函数调用结束后又是怎么返回的?了解函数栈帧的创建和销毁之后,这些你就都明白了。不同的编译器函数栈帧的创建是略有差异的,具体的细节则取决于编译器的实现。对于初学者而言,越高级的编译器越不容易学习和观察,所以不要使用太高级的编译器,本博客在学习栈帧的创建和销毁时使用的是VS2013。   

1、函数栈帧相关概念     

        在了解函数栈帧之前需要先了解的什么是寄存器。寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。比如eax、ebx、ecx、edx、ebp 和esp等等。最值得注意的两个寄存器是ebp (栈底指针)和esp(栈顶指针),这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

        push 压栈:给栈顶放元素进去

        pop 出栈:从栈顶删除元素

        接着我们来了解函数栈帧的概念。

        函数栈帧是编译器实现函数调用的一种数据结构,记录函数调用时产生的相关信息。每一个函数调用,都要在栈区创建一个空间。以main函数为例,我们来看看是如何分配栈空间的:

C语言进阶——函数栈帧的创建和销毁_第1张图片

 2、函数栈帧的创建与销毁

          我们以一段代码为例,看看函数栈帧是如何创建的。

C语言进阶——函数栈帧的创建和销毁_第2张图片

 对上方代码进行调试,选中调用堆栈窗口,我们发下其实main函数也是被其他函数调用的。

C语言进阶——函数栈帧的创建和销毁_第3张图片

        调用main函数的函数是__tmainCRTStartup函数,而__tmainCRTStartup函数又是在mainCRTStartup函数内部调用的。

        为了帮助我们更清楚的看大栈帧的创建过程,我们看看上述代码对应的反汇编代码。

int main()
{
007318B0  push        ebp  
007318B1  mov         ebp,esp  
007318B3  sub         esp,0E4h  
007318B9  push        ebx  
007318BA  push        esi  
007318BB  push        edi  
007318BC  lea         edi,[ebp-24h]  
007318BF  mov         ecx,9  
007318C4  mov         eax,0CCCCCCCCh  
007318C9  rep stos    dword ptr es:[edi]  
007318CB  mov         ecx,73C003h  
007318D0  call        0073131B  

    int a = 10;
007318D5  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
007318DC  mov         dword ptr [ebp-14h],14h  
    int c = 0;
007318E3  mov         dword ptr [ebp-20h],0  
    c = Add(a, b);
007318EA  mov         eax,dword ptr [ebp-14h]  
007318ED  push        eax  
007318EE  mov         ecx,dword ptr [ebp-8]  
007318F1  push        ecx  
007318F2  call        007310B4  
007318F7  add         esp,8  
007318FA  mov         dword ptr [ebp-20h],eax  
    printf("%d\n", c);
007318FD  mov         eax,dword ptr [ebp-20h]  
00731900  push        eax  
00731901  push        737B30h  
00731906  call        007310D2  
0073190B  add         esp,8  

    return 0;
0073190E  xor         eax,eax  
}
00731910  pop         edi  
00731911  pop         esi  
00731912  pop         ebx  
00731913  add         esp,0E4h  
00731919  cmp         ebp,esp  
0073191B  call        00731244  
00731920  mov         esp,ebp  
00731922  pop         ebp  
00731923  ret  

2.1main函数的创建

007318B0  push        ebp  //将edp压入栈帧
007318B1  mov         ebp,esp  //将esp的值赋给edp
007318B3  sub         esp,0E4h  //esp-0E4h 将esp向上(低地址方向)移动4个字节
007318B9  push        ebx  //接下来三行是将 ebx esi edi 压入栈顶
007318BA  push        esi  
007318BB  push        edi  
007318BC  lea         edi,[ebp-24h]  //然后将main函数的函数栈帧初始化为0cccccccch
007318BF  mov         ecx,9  
007318C4  mov         eax,0CCCCCCCCh  
007318C9  rep stos    dword ptr es:[edi] 

C语言进阶——函数栈帧的创建和销毁_第4张图片

 2.2main函数中变量的创建

int a = 10;
007318D5  mov      dword ptr [ebp-8],0Ah  //把0Ah赋值给内存地址为ebp-8中的双字节的空间
 int b = 20;
007318DC  mov      dword ptr [ebp-14h],14h  //把14h赋值给内存地址为ebp-14h中的双字节的空间
 int c = 0;
007318E3  mov      dword ptr [ebp-20h],0  //把0赋值给内存地址为ebp-20h中的双字节的空间

C语言进阶——函数栈帧的创建和销毁_第5张图片

2.3 Add函数栈帧的创建

当abc变量创建好了之后,开始调用add函数。分别将eax(20)和ecx(10)压入栈顶。实际上就是在为Add函数传参

c = Add(a, b);
007318EA  mov         eax,dword ptr [ebp-14h]  
007318ED  push        eax  
007318EE  mov         ecx,dword ptr [ebp-8]  
007318F1  push        ecx  
007318F2  call         007310B4  
007318F7  add         esp,8  

C语言进阶——函数栈帧的创建和销毁_第6张图片

         接着使用call指令将call指令的下一条指令的地址压入栈顶,等Add函数调用结束后,就会回到call指令的下一条指令继续执行。

C语言进阶——函数栈帧的创建和销毁_第7张图片

        进入Add函数后,前面的几条指令跟进入main之前的几条指令一样,给函数准备栈帧并对其进行初始化。

C语言进阶——函数栈帧的创建和销毁_第8张图片

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

2.4 Add函数栈帧的销毁

00731910  pop         edi  //将esp所指的地址的值赋给edi,再将esp的值增加4字节
00731911  pop         esi   //将esp所指的地址的值赋给esi,再将esp的值增加4字节
00731912  pop         ebx   //将esp所指的地址的值赋给ebx,再将esp的值增加4字节
00731920  mov         esp,ebp  //将ebp的值赋给esp,并不是将ebp所指向的内存空间的值赋给esp
00731922  pop         ebp  //将esp所指的地址的值赋给ebp,再将esp的值增加4字节
00731923  ret                //执行完该命令后,自动返回call指令的下一行

C语言进阶——函数栈帧的创建和销毁_第9张图片

现在我们可以轻松回答文章开篇提出的问题。

1)局部变量是怎么创建的?

        函数开辟栈帧空间初始化之后,为局部变量分配空间。

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

        局部变量的值是随机放进去的,在初始化之后才进行了覆盖。

3)函数是怎么传参的?

        调用函数时,先push压栈,通过指针偏移量来传参。

4)形参和实参的关系是什么?

        形参压栈开辟空间,它与实参在空间上是独立的,它是实参的一份临时拷贝

5)函数是如何被调用的?

        开辟栈空间,然后传参,调用

6)函数调用结束后又是怎么返回的

        将返回值放在寄存器中,以文中的Add函数为例,当返回函数时,放入局部变量c中

你可能感兴趣的:(C基础,C进阶,c语言)