[c/c++]——函数栈帧

22 篇文章 2 订阅

函数栈帧

相信作为程序员的你对于写代码来说可能已经是行云流水了,就拿最基础的c语言来说,c语言的基本组成单元是函数,而通过函数之间的互相调用就构成了我们的程序。但是,写了这么久的代码,你真的知道函数间底层是怎么进行调用的么?今天就让我们透过汇编带你一探函数栈帧中的奥秘。

这幅图相信大家一定很熟悉,在学习c语言的时候老师没少提到这个东西,但是今天我们主要来聊一聊图中的栈,栈是向下生长的,也就是说高地址在上,低地址在下,这一点非常重要。
在这里插入图片描述

函数栈帧的概念

先来听听函数栈帧的概念:栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的 一种数据结构。

实际上我们可以简单的将函数栈帧理解为他是一个函数执行的环境,在栈上某一部分保存了函数的局部变量返回信息等。每一个函数都有一个自己相对应的栈帧,这些栈帧是如何相互联系活动的,接着向下看。

函数栈的EBP和ESP

要想知道函数调用返回的原理,你就不得不了解俩个寄存器ebp和esp,现在你写一个main函数,函数内部什么都不要写,然后转到反汇编。

int main()
{
	return 0;
}

如下图看出,main函数一上来就push了ebp和mov了esp这两个东西,这两个东西是函数栈中非常重要的俩个寄存器:

  • EBP:基址指针寄存器,指向当前堆栈底部

  • ESP:堆栈(Stack)指针寄存器,指向堆栈顶部
    在这里插入图片描述
    如果你还不明白就看下图:此时ebp指向main函数的栈帧底部(高地址),esp指向main函数的栈帧顶部(低地址)
    在这里插入图片描述
    此时你已经知道怎么判断当前调用函数的栈帧起始到终止位置了,但是这还不够,要想真正理解,你还要学会几条简单的汇编指令和其他其他寄存器代表的含义:(重要的已经加粗,你懂我意思,哈哈)

  • EAX:累加(Accumulator)寄存器,常用于函数返回值

  • ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器

  • EDX:数据(Data)寄存器,常用于乘除法和I/O指针

  • ESI:源变址寄存器

  • DSI:目的变址寄存器

  • EIP:指令寄存器,指向下一条指令的地址(有的地方会用pc)

  • mov:把一个字节、字或双字的操作数从源位置传送到目的位置

  • push:实现压入操作的指令是PUSH指令,也就是在图中,它把内容压进栈中。

  • pop:实现弹出操作的指令是POP指令

  • lea:LEA指令的功能是将源操作数、即存储单元的有效地址(偏移地址)传送到目的操作数

这里讲一下push和pop操作后esp会发生的变化,我们后边会用到,你需要对这个进行理解:pop则相反
在这里插入图片描述
所以我们现在回到刚才那一段反汇编,既然我们在代码块中什么都没有写,说明这是初始化栈帧的一个操作,也就意味着无论什么情况下都会出现这段代码,不妨现在带领大家一句一句的刨析这段代码:(不要觉得看反汇编恶心,其实一步一步看会觉得很有趣)

int main()
{
003C54A0  push        ebp  //将ebp寄存器压入栈低,保存ebp
003C54A1  mov         ebp,esp  //把esp的值传递给ebp,设置当前ebp
003C54A3  sub         esp,0C0h  //sub是减的意思,所以esp-0C0h就是给main函数栈帧分配空间
003C54A9  push        ebx  //接下来3句是压入需要用到的寄存器
003C54AA  push        esi  
003C54AB  push        edi  
003C54AC  lea         edi,[ebp-0C0h]  //将栈帧大小放入edi中,edi也是一个寄存器,解释为目的变址寄存器
003C54B2  mov         ecx,30h  //函数空间的dword数目,0xF0>>2 = 0x3C,dword是注册表键值
003C54B7  mov         eax,0CCCCCCCCh  
003C54BC  rep stos    dword ptr es:[edi]  //rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
	return 0;
003C54BE  xor         eax,eax  
}

这时我们对main函数的栈帧完成了初始化:
在这里插入图片描述
如果你已经看明白了如何初始化一个栈帧,现在我们就开始研究一个函数在调用另一个函数时会发生什么。

函数调用

接下来的讲解基于一段简单的代码:

void fun(int x, int y)
{
	int m, n;
	m = x;
	n = y;
}
int main()
{
	int a = 10;
	int b = 20;
	fun(a, b);
	return 0;
}

先来看看main函数的反汇编:
在这里插入图片描述
此时main函数的栈帧中就像这样:
在这里插入图片描述
接着call函数fun就是调用这个函数的同时,编译器会把返回地址填入当前函数栈帧中:
在这里插入图片描述
在这里插入图片描述
现在我们就要跳到fun函数了,睁大眼睛,接着向下看:下图是fun函数的反汇编我们接着来刨析代码
在这里插入图片描述

void fun(int x, int y)
{
01274D00  push        ebp  //注意这里非常关键,这里push mian函数的ebp因为现在到了一个新的函数,
//也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起来(因为调用完需要恢复mainebp),栈顶
//是不用保存的,因为上一个栈帧的顶部讲会是func的栈帧底部。(两栈帧相邻的)
01274D01  mov         ebp,esp  
01274D03  sub         esp,0D8h  
01274D09  push        ebx  
01274D0A  push        esi  
01274D0B  push        edi  
01274D0C  lea         edi,[ebp-0D8h]  
01274D12  mov         ecx,36h  
01274D17  mov         eax,0CCCCCCCCh  
01274D1C  rep stos    dword ptr es:[edi]  //以上代码是对fun函数栈帧的初始化
	int m, n;
	m = x;
01274D1E  mov         eax,dword ptr [x]  
01274D21  mov         dword ptr [m],eax  
	n = y;
01274D24  mov         eax,dword ptr [y]  
01274D27  mov         dword ptr [n],eax  
}

现在栈帧如下图:
在这里插入图片描述
函数体执行了什么就不做讲解,我们现在来看看返回过程:
在这里插入图片描述
fun返回:

01274D2A  pop         edi  
01274D2B  pop         esi  
01274D2C  pop         ebx  
01274D2D  mov         esp,ebp  //将esp的位置恢复到当前ebp的位置,当前ebp就是main函数的栈帧顶部
01274D2F  pop         ebp  //弹出ebp,恢复ebp的值
01274D30  ret //把返回地址写入EIP中,相当于pop EIP

main返回:

012754D4  call        fun (01271096h)  
012754D9  add         esp,8  //esp加8就是实参出栈
	return 0;
012754DC  xor         eax,eax  
}
012754DE  pop         edi  //因为系统调用main函数,所以main也需要返回
012754DF  pop         esi  
012754E0  pop         ebx  
012754E1  add         esp,0D8h  
012754E7  cmp         ebp,esp  
012754E9  call        __RTC_CheckEsp (012712E9h)  
012754EE  mov         esp,ebp  
012754F0  pop         ebp  
012754F1  ret  

所以,fun返回到main从01274D2D的示意图如下:
在这里插入图片描述
到这里就完成了函数的调用过程。

总结

这就是我对函数栈帧的一点理解,如果有什么问题请各位同学指出,我一定虚心改正,补充说明一下,在vs下有时候调用函数反汇编会检查是否有栈溢出,所以可能和我的示例代码不太一样,还有就是不要完全相信我,自己动手调式观察或许会有不一样的收获。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值