《C语言》函数栈帧的创建与销毁–(内功)

目录

一、寄存器

二、内存

          演示环境:window 10 && vs2013

         三、演示函数栈帧的创建销毁过程

         四、总结



一、寄存器

        eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。如果用C语言来解释,可以把这些寄存器当作变量看待。

 这些32位寄存器有多种用途,但每一个都有各自的特别之处。
eax:累加寄存器,相对于其他寄存器,在运算方面比较常用。

ebx:基地址寄存器,作为内存偏移指针使用。

ecx:计数器,用于特定的技术。

edx:作为EAX的溢出寄存器,(除法产生的余数)。

eip:存储CPU下次所执行的指令地址(存放指令偏移地址)。

esp:指针的寄存器,用于堆栈操作。被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。在32位平台上,esp每次减少4字节。
ebp:基址指针,指栈的栈底指针。

二、内存

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

演示环境:window 10 && vs2013

原因:编译器越高级,其中处理的越会繁琐,越高级的编译器,越不容易学习和观察。

同时在不同的编译器下,函数的调用过程中的栈帧的创建是略有差异的,具体细节取决于编译器的实现;


三、演示函数栈帧的创建销毁过程

先来了解两个寄存器 分别为:ebp,esp

这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

//下面为本次演示的代码块
#include<stdio.h>
int Add(int x, int y){
	int z = 0;
	z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d", c);
	system("pause");
	return 0;
}

首先当代码被编译的时候 main函数会在内存中申请到一块空间

 然后我们按下F10,并且打开调试的堆栈窗口可以发现,main函数被调用起来了

 那么我们新的问题来了 main函数被谁调用了?

 莫慌  我们快进到代码运行结束

 

 我们可以看到, main 函数调用之前,是由 invoke_main 函数来调用main函数。那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈 帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。

那接下来我们从main函数的栈帧创建开始说起: 

那么栈帧的创建又是如何实现的呢?

我们来转到反汇编来look一下

int main()
{
004A1410  push        ebp  
004A1411  mov         ebp,esp  
004A1413  sub         esp,0E4h  
004A1419  push        ebx  
004A141A  push        esi  
004A141B  push        edi  
004A141C  lea         edi,[ebp-0E4h]  
004A1422  mov         ecx,39h  
004A1427  mov         eax,0CCCCCCCCh  
004A142C  rep stos    dword ptr es:[edi]  
	int a = 10;
004A142E  mov         dword ptr [a],0Ah  
	int b = 20;
004A1435  mov         dword ptr [b],14h  
	int c = 0;
004A143C  mov         dword ptr [c],0  
	c = Add(a, b);
004A1443  mov         eax,dword ptr [b]  
004A1446  push        eax  
004A1447  mov         ecx,dword ptr [a]  
004A144A  push        ecx  
004A144B  call        _Add (04A10E1h)  
004A1450  add         esp,8  
004A1453  mov         dword ptr [c],eax  
	printf("%d", c);
004A1456  mov         esi,esp  
004A1458  mov         eax,dword ptr [c]  
004A145B  push        eax  
004A145C  push        4A5858h  
004A1461  call        dword ptr ds:[4A9114h]  
004A1467  add         esp,8  
004A146A  cmp         esi,esp  
004A146C  call        __RTC_CheckEsp (04A113Bh)  
	system("pause");
004A1471  push        4A58B0h  
004A1476  call        _system (04A11E0h)  
004A147B  add         esp,4  
	return 0;
004A147E  xor         eax,eax  
}
004A1480  pop         edi  
004A1481  pop         esi  
004A1482  pop         ebx  
004A1483  add         esp,0E4h  
004A1489  cmp         ebp,esp  
004A148B  call        __RTC_CheckEsp (04A113Bh)  
004A1490  mov         esp,ebp  
004A1492  pop         ebp  
004A1493  ret  

可以看到是执行了非常多道“工序” 那么我将它为分三个模块来说

一、main函数调用

 push ebp //把ebp寄存器中的值进行压栈,此时的ebp中存放的是invoke_main函数栈帧的ebp,esp-4

 mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp

 sub esp,0E4h //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。

 push ebx //将寄存器ebx的值压栈,esp-4

 push esi //将寄存器esi的值压栈,esp-4

 push edi //将寄存器edi的值压栈,esp-4

 lea edi,[ebp-0E4h] //先把ebp-0E4h的地址,放在edi中

mov ecx,39h//把39放在ecx中

rep stos dword ptr es:[edi]//将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCC

 二、main函数中的核心代码

 

dword ptr [a],0Ah //将10存储到0Ah的地址处,0Ah的位置其实是a变量

mov dword ptr [b],14h //将20存储到14h的地址处,14h的位置其实是b变量

mov dword ptr [c],0 //将0存储到0的地址处,0的位置其实是c变量

 

 三、函数传参

  1. c = Add(a, b);

  2. //调用Add函数时的传参

  3. //其实传参就是把参数push到栈帧空间中。

 

moveax,dword ptr [b]//传递b,将ebp-14h处放的20放在eax寄存器中

push eax //将eax的值压栈,esp-4

mov ecx,dword ptr [a]//传递a,将0Ah处放的10放在ecx寄存器中

push ecx //将ecx的值压栈,esp-4

call _Add (04A10E1h)//跳转调用函数,将下一条指令的地址压入栈中        

add esp,8

 mov dword ptr [c],eax

 

 call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈 操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

int Add(int x, int y)
{
00BE1760 push ebp //将main函数栈帧的ebp保存,esp-4
00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
00BE1769 push ebx //将ebx的值压栈,esp-4
00BE176A push esi //将esi的值压栈,esp-4
00BE176B push edi //将edi的值压栈,esp-4
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
z = x + y;
//接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp,ebp
00BE1784 pop ebp
00BE1785 ret
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

将main函数的 ebp 压栈

计算新的 ebp 和 esp

将 ebx , esi , edi 寄存器的值保存

计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的 参数,这就是形参访问。

将求出的和放在 eax 寄存器准备带回

 

00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00BE1780 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
00BE1781 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
00BE1782 mov esp,ebp //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈
帧空间
00BE1784 pop ebp //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。
00BE1785 ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行

调用完Add函数被释放后 寄存器中存储的就是Add函数的返回值 这也可以解释为何调用函数只能传回一个值

 


四、总结

希望可以对大家有所帮助,如有错误请大家指出 万分感谢!

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>