C语言:浅谈函数的调用过程及栈帧的创建和销毁

  1. 说起函数调用,我们可能很快就想到:程序从main函数走起,遇到调用函数的语句,就跳转到此函数所在的语句块执行此函数,执行完之后再返回main函数继续执行程序。但是这只是笼统的描述,其实在函数内部,函数调用要经过一系列的复杂的过程,下面为大家一一详细叙述。
    1. 说到函数调用,我们不可避免的要说到栈帧的创建和销毁。函数调用过程要为函数开辟空间,用于本次函数的调⽤用中临时变量的保存、现场保护。这块栈空间,我们称之为函数栈帧。首先应该明白,栈是从高地址向低地址延伸的。而说到函数栈帧,我们不可避免的要说到两个寄存器:寄存器ebp和寄存器esp。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。这两个寄存器用于维护函数栈帧。
    2. 介绍完基本概念,我们使用一个具体的例子来看看函数的调用过程及栈帧的创建和销毁具体是怎么实现的。首先看代码:
#include<stdio.h>
#include<stdlib.h>

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
    printf("ret = %d\n", ret);
    system("pause");
    return 0;
}

这个代码时main函数调用了一个简单的Add函数,但足以说明问题,下面我们来分析一下。首先看下它的汇编代码

00401060   push        ebp
00401061   mov         ebp,esp
00401063   sub         esp,4Ch
00401066   push        ebx
00401067   push        esi
00401068   push        edi
00401069   lea         edi,[ebp-4Ch]
0040106C   mov         ecx,13h
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]
12:       int a = 10;
00401078   mov         dword ptr [ebp-4],0Ah
13:       int b = 20;
0040107F   mov         dword ptr [ebp-8],14h
14:       int ret = Add(a, b);
00401086   mov         eax,dword ptr [ebp-8]
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx
0040108E   call        @ILT+0(_Add) (00401005)
00401093   add         esp,8
00401096   mov         dword ptr [ebp-0Ch],eax
15:       printf("ret = %d\n", ret);
00401099   mov         edx,dword ptr [ebp-0Ch]
0040109C   push        edx
0040109D   push        offset string "ret = %d\n" (00424024)
004010A2   call        printf (00401200)
004010A7   add         esp,8
16:       system("pause");
004010AA   push        offset string "pause" (0042401c)
004010AF   call        system (004010f0)
004010B4   add         esp,4
17:       return 0;
004010B7   xor         eax,eax
18:   }
004010B9   pop         edi
004010BA   pop         esi
004010BB   pop         ebx
004010BC   add         esp,4Ch
004010BF   cmp         ebp,esp
004010C1   call        __chkesp (00401280)
004010C6   mov         esp,ebp
004010C8   pop         ebp
004010C9   ret

我们来文字具体分析一下它的步骤
一、main函数执行之前首先要执行mainCRTStart函数,为其开辟一块内存空间。
二、执行main函数

  1. push ebp 将ebp压入栈
  2. 将esp移向栈顶
  3. mov ebp esp 将esp赋给ebp,即将ebp移到esp所指向的位置,这时esp,ebp指向同一块内存空间。
  4. esp向上移动,为main函数开辟空间,此时esp和ebp维护一块新的空间,即main函数的空间。
  5. 向main函数的栈帧里压入ebx,esi,edi三个寄存器,同时esp移向栈顶。
  6. 为main函数开辟的空间进行初始化。
  7. 将栈底指针减4,向上移动4个字节,为变量a开辟空间。即创建变量a,并将a的值放入内存。
  8. 将栈底指针再向上移动4个字节,为变量b开辟空间。即创建变量b, 并将b的值放入内存。
  9. 将栈底指针再向上移动4个字节,为变量ret开辟空间,即创建变量ret,并将ret的值放入内存。
  10. 将ebp-8,即将变量b的值放入eax寄存器,这一块空间即是实参b的临时拷贝。之后将esp移向栈顶。
  11. 将ebp-4,即将变量a的值放入eax寄存器,这一块空间即是实参a的临时拷贝。之后将esp移向栈顶。
    三、调用Add函数
    1.push ebp 将ebp压入栈中
    2.将esp移向栈顶
    3.mov ebp,esp.将esp赋给ebp,即将ebp移动到esp所指向空间。此时esp和ebp指向同一块空间。
    4.esp向上移动,为Add函数欲开辟空间,此时esp和ebp维护一块新的空间,即Add函数的空间。
    5.为Add函数开辟的空间进行初始化。
    6.将栈底指针ebp-4,即栈底向上移动4个字节,为变量z开辟空间,创建变量z.
    7.将栈底指针ebp+8,即ebp向下移动8个字节,此时ebp指向exa寄存器,即刚才所存放的变量a的值的空间。
    8.将栈底指针ebp+12,即ebp向下移动12个字节,此时ebp指向exa寄存器即刚才所存放的变量b的值的空间。
    9.将形参x和形参y进行相加。
    10.将相加的结果放入exa寄存器。
    11.将寄存器edi,esi,ebx弹出栈,esp向下移动。
    12.将ebp赋给esp,弹出ebp,esp与ebp回到原来维护main函数的空间。
    Add函数栈帧被销毁。
    13.返回main函数。
    14.esp+8,销毁刚才创建的存放变量a和变量b的值的空间,即销毁刚才创建的形参。
    15.将刚才存放结果的寄存器放入变量ret所指向的空间,即将函数所返回的结果放入变量ret的空间。
    至此,Add函数调用过程即相关函数栈帧的创建于销毁完成。

下面是函数调用过程的简单步骤图

这里写图片描述

猜你喜欢

转载自blog.csdn.net/windyj809/article/details/79962199