函数栈帧的理解

一、什么是函数栈帧

        每当函数被调用的时候,都会向内存空间申请一定的地址(地址的大小由编译器决定),这一块内存就被成为函数栈帧。

二、函数栈帧的作用

  1. 用来存放函数以及函数的参数。
  2. 用来存放临时变量。
  3. 用来连接上下内容。

在栈中有这一些寄存器(这些寄存器与内存是独立分开的)每个函数的栈帧是由esp和ebp来维护的。

eax:通用寄存器,保留临时数据,常用于返回值

ebx:通用寄存器,保留临时数据

ebp:栈底寄存器

esp:栈顶寄存器

eip:指令寄存器,保存当前指令的下一条指令的地址

三、理解函数栈帧

        我们那下面的一段C语言代码,进行反编汇来进行理解。

#define _CRT_SECURE_NO_WARNINGS 1
#include 
int Add(int x, int y)
{
	int res = 0;
	res = x + y;
	return res;
}
int main()
{
	int a = 10;
	int b = 20;
	int res = 0;
	res=Add(a, b);
	printf("%d", res);
	return 0;
}

我们在调试的时候可以看到函数栈帧的理解_第1张图片

 

在main函数调用前,先进行了invoke_main函数的调用。这里我们不去深度研究这个函数的作用。

所以我们就可以知道,在我们写的代码中,至少有三个函数(main、invoke_main、Add)拥有自己的栈帧。这里我们进行到反编汇。

int main()
{
00A618B0  push        ebp  //把ebp的值压入栈中,这里的ebp的值存放的是invoke_main这个函数的栈底地址。
00A618B1  mov         ebp,esp  //将esp(invoke_main函数的栈顶地址)的值赋给ebp
00A618B3  sub         esp,0E4h  //这个0E4h就是编译器为我们的main函数申请到的函数地址,这里将esp的值减去0E4h
00A618B9  push        ebx  
00A618BA  push        esi  
00A618BB  push        edi  //进行了三个压栈操作
//下面的代码是对栈帧空间进行初始化
00A618BC  lea         edi,[ebp-24h]  //把ebp-24h的值赋给edi
00A618BF  mov         ecx,9  //把9赋值给ecx
00A618C4  mov         eax,0CCCCCCCCh  //把0CCCCCCCCh赋值给eax
00A618C9  rep stos    dword ptr es:[edi]  // 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC 

00A618CB  mov         ecx,offset _F6609206_zhanzhentest@c (0A6C003h) 


 
接下来进行到函数的内部:
	int a = 10;
00A618D5  mov         dword ptr [ebp-8],0Ah  //在ebp-8的地址空间为a开辟一个内存空间,存放10
	int b = 20;
00A618DC  mov         dword ptr [ebp-14h],14h  //在ebp-14h的地址空间b 开辟一个内存空间,存放20
int res = 0;
00A618E3  mov         dword ptr [ebp-20h],0  //在ebp-20h的地址空间res 开辟一个内存空间,存放0
res=Add(a, b);
00A618EA  mov         eax,dword ptr [ebp-14h]  //此时ebp-14h的值为b的值,将b的值赋给eax
00A618ED  push        eax  //把eax的值进行压栈 esp-4
00A618EE  mov         ecx,dword ptr [ebp-8]  //此时ebp-8的值是a的值,把a的值赋给ecx

00A618F1  push        ecx  //把ecx的值进行压栈 esp-4
//我们可以看到,在创建形参的时候,形参是实参的一份临时拷贝,放在原函数的栈帧中
//接下来会用到call指令,这个指令的运行结果途中所示。
/*call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。这样做的作用是,当函数调用完后,栈帧被销毁后,可以继续找到调用函数的下一条指令的地址,进而继续执行程序。*/
//我们可以看到 call指令的下一条指令的地址是00A618F7
00A618F2  call        00A610B4  //调用Add函数
//这时我们跳转到函数内部
int Add(int x, int y)
{
00D91770  push        ebp  //把main函数的ebp的值压入栈中,esp-4
00D91771  mov         ebp,esp  //把esp的值赋给ebp
00D91773  sub         esp,0CCh  //将esp减去0CCh
00D91779  push        ebx  
00D9177A  push        esi  
00D9177B  push        edi  //三个压栈操作
00D9177C  lea         edi,[ebp-0Ch]  
00D9177F  mov         ecx,3  
00D91784  mov         eax,0CCCCCCCCh  
00D91789  rep stos    dword ptr es:[edi]  //初始化Add函数的栈帧
00D9178B  mov         ecx,0D9C003h  
00D91790  call        00D9131B  
	int res = 0;
00D91795  mov         dword ptr [ebp-8],0  
	res = x + y;
00D9179C  mov         eax,dword ptr [ebp+8]  //把a’的值放在eax寄存器中
00D9179F  add         eax,dword ptr [ebp+0Ch]  //把b’的值与eax相加后即存在eax中
00D917A2  mov         dword ptr [ebp-8],eax  //把eax的值赋值给res
	return res;
00D917A5  mov         eax,dword ptr [ebp-8]  //把return的res的值赋给eax寄存器
}
//下面进行栈帧的销毁,我们会发现,即使函数的栈帧被销毁了,我们的eax寄存器依然存着我们的返回值
00D917A8  pop         edi  //出栈操作,把栈顶的值赋给edi并删除,esp-4
00D917A9  pop         esi  //出栈操作,把栈顶的值赋给esi并删除,esp-4
00D917AA  pop         ebx  //出栈操作,把栈顶的值赋给ebi并删除,esp-4
00D917AB  add         esp,0CCh  //0CCh与我们当时申请到的空间相同,所以删除掉
00D917B1  cmp         ebp,esp  
00D917B3  call        00D91244  
00D917B8  mov         esp,ebp  //把ebp的值赋给esp
00D917BA  pop         ebp  //把栈顶弹出赋值给ebp,此时ebp指向main函数的ebp
00D917BB  ret  //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指 
令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行


也就是进行到下面的指令。



00A618F7  add         esp,8  //相当于把a’和b’进行销毁
00A618FA  mov         dword ptr [ebp-20h],eax  //把eax的值赋给了我们main中的res
//接着进行以下操作
	printf("%d", res);
00A618FD  mov         eax,dword ptr [ebp-20h]  
00A61900  push        eax  
00A61901  push        0A67B30h  
00A61906  call        00A610D2  
00A6190B  add         esp,8  

可以借助下图来理解栈帧的创建过程函数栈帧的理解_第2张图片

 

你可能感兴趣的:(C语言,数据结构,c#)