函数栈帧(函数调用执行过程剖析)

目录

函数栈帧是什么?

内存分区

寄存器

汇编指令 

 栈帧创建与销毁过程

函数执行之前的准备工作

函数执行

函数执行结束,进行函数返回

ebp回到上一个栈底

销毁形参

回到上一栈帧 

查看汇编指令

​编辑


前言

在C语言编写时,我们总会把一些功能单独写成一个函数,在主函数中调用,只需要在调用时通过函数名将实参传给形参就实现了整个函数调用过程,但实际的调用过程底层很复杂,这其中关系到函数栈帧。

函数栈帧是什么?

栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息,比如该函数的返回地址和局部变量寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)

内存分区

栈区:从高地址向低地址延伸的,主要用来存放局部变量,函数调用开辟的空间,与堆共享一段空间。

堆区:由低地址向高地址增长,动态开辟的空间就在这里(malloc,realloc,calloc,free),与栈共享一段空间。

静态区:主要存放全局变量和静态变量。 

寄存器

ebpebp是基址指针,保存调用者函数的地址,总是指向当前栈帧栈底
espesp是被调函数指针,总指向函数栈栈顶
esx累加器,用来乘除法,与函数返回值(本篇主要关注第二个功能)
eax通用寄存器,保存临时数据,常用于返回值
eip指令寄存器,保存当前指令的下一条指令的地址

汇编指令 

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令

简单来讲,esp和ebp是两个指针ebp指向当前栈帧栈底,esp指向函数栈栈顶。

 栈帧创建与销毁过程

#define _CRT_SECURE_NO_WARNING
#include<stdio.h>
#include<stdlib.h>
int Add(int a, int b)
{
	int c = 0;
	c = a + b;
	return c;
}
int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	printf("ret = %d\n", ret);
	system("pause");
	return 0;
}

 

函数执行之前的准备工作

将ADD函数需要的参数a=10和b=20入栈

 

函数执行

保护现场(保护ebp)

由于马上要建立新的栈帧,因此对ebp和esp都得变动,为了在调用add函数后能将ebp还原到初始位置,因此需要对ebp进行保护,即将ebp的值压入栈。 

 创建调用函数的栈帧空间

令ebp指向当前的esp位置,并且创建一块合适大小的空间

 保存局部变量

将ADD函数创建的变量int c=0放入开辟的栈帧空间

 进行运算

执行c=a+b

函数执行结束,进行函数返回

存储返回值

达到目的ADD(a,b),现在我们希望回到main函数中继续往下执行,所以要对ADD函数桢进行销毁,但是main'函数还没有拿到ADD的返回值,此时就是前面提到的eax寄存器发挥作用,我们将返回值存储到eax寄存器中。

ebp回到上一个栈底

此时ebp拿到之间存储的上一栈帧栈底的值,回到相应的位置,于此同时,存储的ebp没有用了,也将被销毁。

销毁形参

形参进行销毁(所以,形参的改变不会影响实参,因为地址不同)

回到上一栈帧 

main函数拿到返回值,此时注意,这个上一栈帧代表的是什么,我们直到main其实也是一个函数,所以也有自己的栈帧,所以说这个上一栈帧就是main函数的栈帧,所以此时main函数的sum拿到eax的值,所以说,我们只有一个寄存器,因此C语言函数只能由一个返回值。

查看汇编指令

int Add(int a, int b)
{
006D1820  push        ebp      //push指令会压入ebp寄存器
006D1821  mov         ebp,esp      //move指令会把esp的值存放到ebp中,相当于产生了add函数的栈帧
006D1823  sub         esp,0C0h      //这里大可先不必多研究
006D1829  push        ebx      
006D182A  push        esi  
006D182B  push        edi  
	return a + b;
006D182C  mov         eax,dword ptr [a]  
006D182F  add         eax,dword ptr [b]  
}
int Add(int a, int b)
{
00681800  push        ebp  //push指令会压入ebp寄存器
00681801  mov         ebp,esp  //move指令会把esp的值存放到ebp中,相当于产生了add函数的栈帧
00681803  sub         esp,0CCh  //这里大可先不必多研究
00681809  push        ebx  //将寄存器ebx的值压栈
0068180A  push        esi  //将寄存器esi的值压栈
0068180B  push        edi  //将寄存器esi的值压栈
	int t = 0;
0068180C  mov         dword ptr [t],0  
	t = a + b;
00681813  mov         eax,dword ptr [a]  //exa通用寄存器,保留临时数据a
00681816  add         eax,dword ptr [b]  //exa通用寄存器,保留临时数据a
00681819  mov         dword ptr [t],eax  //把exa寄存器的值交给t
	return t;
0068181C  mov         eax,dword ptr [t]  //将t的值交给eax寄存器
}

 创建add函数栈帧,创建临时变量,计算后将结果存在eax,由eax返回,销毁add栈帧。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
函数调用过程是程序中常见的一种操作,它通常涉及到参数传递、栈帧的建立与销毁、返回值的传递等多个方面。从汇编的角度来看,函数调用过程可以分为以下几个步骤: 1. 将函数的参数压入栈中。在调用函数,需要将函数所需的参数传递给它。这些参数通常以一定的顺序压入栈中,以便在函数内部使用。在 x86 架构中,参数的传递是通过将参数压入栈顶实现的。 2. 调用函数函数调用的指令通常是 CALL 指令。在调用函数前,需要将函数的入口地址压入栈中,以便在函数执行完毕后返回到调用位置。CALL 指令会将当前的程序计数器(PC)压入栈中,并将函数的入口地址作为新的 PC。 3. 建立栈帧。在函数被调用,需要为函数建立一个独立的栈帧,以便在函数内部使用局部变量和临变量。栈帧通常包括以下几个部分:返回地址、旧的基址指针、局部变量和临变量。在 x86 架构中,栈帧的建立是通过将 ESP 寄存器减去一个固定的值实现的。 4. 执行函数。在函数被调用后,CPU 会跳转到函数的入口地址并开始执行函数函数内部可以通过栈中的参数和局部变量完成相应的计算和操作。 5. 返回值传递。在函数执行完毕后,需要将函数的返回值传递给调用者。在 x86 架构中,函数的返回值通常通过 EAX 寄存器传递。 6. 销毁栈帧。在函数执行完毕后,需要将栈帧销毁,以便释放栈空间。栈帧的销毁通常是通过将 ESP 寄存器还原到旧的基址指针处实现的。 7. 返回到调用位置。在函数执行完毕后,需要返回到函数被调用的位置。在 x86 架构中,返回指令通常是 RET 指令。RET 指令会将栈顶的返回地址弹出,并将其作为新的 PC。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小谢%同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值