函数的调用过程,栈帧的创建和销毁

本文详细解析了函数调用的过程,以Add函数为例,通过C语言代码深入浅出地介绍了函数栈帧、ebp和esp寄存器的作用,以及函数如何开辟、调用、运行和返回。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们平时敲代码可以发现:当我们有一个清晰的思路时,编程就仅仅只是在框架内往其“添砖加瓦”。同一个道理,当我们清楚了解一段代码每个函数的开辟、调用、运行、返回时,那我们学习语言时往往事半功倍。所以,我们需要掌握函数的调用过程。
下面用Add函数为例,阐述函数的调用过程。

#define _CRT_SECURE_NO_WARNINGS 1
#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 sum = Add(a, b);
    printf("sum=%d\n", sum);
    system("pause");
    return 0;
}

当程序调试但main()没被调用时

我们使用【调试-调用堆栈】可以发现:在main()函数执行前计算机首先执行了两个函数:

既然执行函数,就要为这两个函数开辟空间。(注意:此时main()还没执行)

在这里为了方便理解首先介绍两个概念:

  • 函数栈帧。我们使用函数时要为函数开辟栈空间,用于本次函数的调用中临时变量的保存、保护现场。这块栈空间我们称之为函数栈帧。
  • ebp,esp寄存器。在函数调用过程中两个寄存器存放了维护这个栈的栈底和栈顶指针。

现在给 mainCRTStartup 函数开辟空间(__tmainCRTStartup函数同理,在此不做介绍)

继续执行代码,进到main(),我们把c代码反汇编:

第一行:push ebp 把ebp压到栈里面去(ebp存的是为mainCRTStartup开辟的地址)
第二行:把esp的值给ebp (esp、ebp指向同一地址)
第三行:esp地址减4Ch (esp地址变小,由于栈空间由高地址指向底地址,esp向上移动,代表为main函数预开辟空间)
第四、五、六行:压栈3次,在栈顶放3个元素
第七行:lea 加载有效地址
第八、九、十行:从edi(地址ebp-4Ch)开始向下拷贝eax(CCCC)的内容到ptr,拷贝ecx(13h)次,每次拷贝dword(双字,4个字节)个空间。


第一行:将局部变量a放到 (当前函数)栈底地址中
第二行:将局部变量b放到 (当前函数)栈底地址-4中
第三、四行:将形参b放到eax中,且压栈
第五、六行:将形参a放到ecx中,且压栈
第七行:call调用函数 (压栈地址 —— call指令下一条指令的地址)

执行call指令之后,按两下f11,跳转到Add函数:

过程创建跟main()函数一样。事实上,每个函数过程都是如此。


第一行:将局部变量z放到 (当前函数)栈底地址中
第二行:将地址ebp+8内的内容(即形参b)赋值给eax
第三行:获取形参a和b相加,结果赋给eax
第四行:将eax赋值给z
第五行:将z的值保存在eax中

函数的返回值通过寄存器返回。到此,函数调用完,开始销毁。

第一、二、三行:pop三次,出栈三次
第四行:由于ebp是当前函数栈底,所以将ebp赋值给esp,使其直接销毁当前函数,跳到上一级函数的栈底处
第五行:pop ebs出栈 将出栈的内容保存到ebp,回到main函数的栈帧(Add函数销毁)
第六行:ret指令会使得出栈一次,并将出栈的内容当做地址。将程序执行跳转到该地址处。


函数返回,返回到add这一行
add+8相当于将两个形参a,b销毁,生命周期结束。
将eax(之前存的是z的返回值)赋值给ret。

以上就是整个代码运行时函数的调用过程。
注:函数开始执行程序代码时,这个时候程序将使用一个运行时堆栈(函数栈帧stack),存储函数的局部变量和返回地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值