函数栈帧创建与销毁

本文详细解释了X86汇编语言中的通用寄存器EAX、EBX、ECX、EDX等的作用,包括它们在加法、内存寻址和函数调用中的角色。同时介绍了函数栈帧的创建过程,以及ESP和EBP寄存器在维护栈帧空间中的作用。
摘要由CSDN通过智能技术生成

寄存器

EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP 寄存器详解-CSDN博客

一般寄存器:AX、BX、CX、DX
AX:累积暂存器,BX:基底暂存器,CX:计数暂存器,DX:资料暂存器
eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。

eax:通用寄存器, 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
,保存临时数据常用于返回值

ebx:通用寄存器,是"基地址"(base)寄存器, 在内存寻址时存放基地址。

ecx:是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

edx: 则总是被用来放整数除法产生的余数

eip:扩展指令指针,指向下一条要执行的指令的地址。

ebp:函数栈帧的栈底指针

esp:函数栈帧的栈顶指针

相关的汇编指令

mov数据转移指令
push数据入栈,同时esp栈顶指针也会随着变化
pop数据弹出指定位置,同时esp栈顶寄存器也要出栈,用一个寄存器esp接收数据
sub减法
add加法
call函数调用,1、压入返回地址2、转入目标函数
jump通过修改eip转入目标函数,进行调用
ret恢复返回地址压入eip

 

函数栈帧

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

每一个函数调用,都是在为每一个函数调用开辟空间,这就是函数栈帧

函数的调用

int Add(int a,int b)
{
	int z = 0;
	z = a + b;
	return z;
}
int main()
{
	int a = 10;
	int b = 4;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

使用vs2022编译器上调试,进入调试,调用堆栈,进入Add函数

发现在main函数之前,是由invoke_main函数调用main函数的,在invoke_main函数调用之前的就不讲了,也是一层层的调用。所以无论main,invoke_main,还是Add都是有自己的函数栈帧都会有寄存器esp和ebp来维护栈帧空间,接下来我们从main函数的栈帧创建开始讲解:

1、修改一下属性便于观察可以排除编译器附带的代码):

改成(否)

2、转到反汇编

int main()
{
//main函数栈帧的创建
008A1840 55                   push        ebp  
008A1841 8B EC                mov         ebp,esp  
008A1843 81 EC E4 00 00 00    sub         esp,0E4h  
008A1849 53                   push        ebx  
008A184A 56                   push        esi  
008A184B 57                   push        edi  
008A184C 8D 7D DC             lea         edi,[ebp-24h]  
008A184F B9 09 00 00 00       mov         ecx,9  
008A1854 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
008A1859 F3 AB                rep stos    dword ptr es:[edi]  
//main函数的核心代码
	int a = 10;
008A185B C7 45 F8 0A 00 00 00 mov         dword ptr [ebp-8],0Ah  
	int b = 4;
008A1862 C7 45 EC 04 00 00 00 mov         dword ptr [ebp-14h],4  
	int ret = 0;
008A1869 C7 45 E0 00 00 00 00 mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);
008A1870 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
008A1873 50                   push        eax  
008A1874 8B 4D F8             mov         ecx,dword ptr [ebp-8]  
008A1877 51                   push        ecx  
008A1878 E8 3C F8 FF FF       call        008A10B9  
008A187D 83 C4 08             add         esp,8  
008A1880 89 45 E0             mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
008A1883 8B 45 E0             mov         eax,dword ptr [ebp-20h]  
008A1886 50                   push        eax  
008A1887 68 30 7B 8A 00       push        8A7B30h  
008A188C E8 46 F8 FF FF       call        008A10D7  
008A1891 83 C4 08             add         esp,8  
	return 0;
008A1894 33 C0                xor         eax,eax  
}

3、函数栈帧的创建

008A1840 55                   push        ebp  //将寄存器地址压入栈中注意这里ebp是invoke_main的栈底地址,然后esp-4(一个指针4个字节)
008A1841 8B EC                mov         ebp,esp //将esp的地址存放到ebp中,ebp向上移动成为main函数的栈底指针
008A1843 81 EC E4 00 00 00    sub         esp,0E4h//这里esp减去16进制0xE4个大小,esp向上移动,成为main函数栈帧的栈顶指针,和ebp维护这段0XE4空间大小的空间,这段空间将存储局部变量,临时数据等
008A1849 53                   push        ebx  //将寄存器ebx的值压入栈中,esp-4
008A184A 56                   push        esi  //将寄存器esi的值压入栈中,esp-4
008A184B 57                   push        edi  //将寄存器edi的值压入栈中,esp-4
//函数调用的时,首先要保存现场,函数调用后,恢复现场,否则子函数的调用将影响到主函数的运行。那么再拿这三个寄存器
//具体说ebx是基址寄存器,也是就访问内存基地址,esi是源操作数地址寄存器,edi是目的地址寄存器
//如果在子函数中被改写,函数结束后并没有改回来,那么主函数将会执行混乱,所以这三个寄存器要push进栈中,并在函数结束后pop回来

//初始化main函数的栈帧空间
//将ebp-24h的地址,放入edi中
//把9放入ecx中
//把0xCCCCCCCC放入eax中
//将从edi到ebp这段空间都初始化成0Xcc
008A184C 8D 7D DC             lea         edi,[ebp-24h]  
008A184F B9 09 00 00 00       mov         ecx,9  
008A1854 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
008A1859 F3 AB                rep stos    dword ptr es:[edi]  

因为在main函数栈帧空间早就初始化成0XCCCCCCCC所以未初始化的变量打印才会“烫烫烫烫”

4、分析main函数中的核心部分

	int a =10;
00BB185B C7 45 F8 0A 00 00 00 mov         dword ptr [ebp-8],0Ah  //将10存储进地址为ebp-8,这个地址就是a的地址
	int b = 4;
00BB1862 C7 45 EC 04 00 00 00 mov         dword ptr [ebp-14h],4  //与上面同理,word为2个字节double word==4个字节
	int ret = 0;
00BB1869 C7 45 E0 00 00 00 00 mov         dword ptr [ebp-20h],0  //同理
	ret = Add(a, b);
00BB1870 8B 45 EC             mov         eax,dword ptr [ebp-14h]//寄存器eax保存[ebp-14h]地址的值4给eax
00BB1873 50                   push        eax  //压入栈中 esp-4
00BB1874 8B 4D F8             mov         ecx,dword ptr [ebp-8]//累加寄存器ecx保存[ebp-8]的值10给eax
00BB1877 51                   push        ecx   //压入栈中 esp-4

//jump函数这里是函数的地址
00BB1878 E8 3C F8 FF FF       call        00BB10B9  
00BB187D 83 C4 08             add         esp,8  
00BB1880 89 45 E0             mov         dword ptr [ebp-20h],eax  

5、函数调用

00BB1878 E8 3C F8 FF FF       call        00BB10B9  
00BB187D 83 C4 08             add         esp,8  
00BB1880 89 45 E0             mov         dword ptr [ebp-20h],eax  

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

这上面的10和4是Add函数的形参x,y,这里的分析很好的说明了函数的传参过程以及值传递调用时,形参是实参的临时拷贝,形参的修改不会影响到实参

进入函数

int Add(int a,int b)
{
00BB1780 55                   push        ebp  //将ebp的值压入栈中,就是地址,esp-4
00BB1781 8B EC                mov         ebp,esp  //esp的值给ebp,ebp移动到esp
00BB1783 81 EC CC 00 00 00    sub         esp,0CCh  //将esp-0cc,esp向上移动
00BB1789 53                   push        ebx  //将ebx压入栈中
00BB178A 56                   push        esi  //将ebx压入栈中
00BB178B 57                   push        edi  //将ebx压入栈中
	int z = 0;
00BB178C C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0  //将0存入[ebp-8]中
	z = a + b;
00BB1793 8B 45 08             mov         eax,dword ptr [ebp+8]  //将[ebp+8]的值存储到eax
00BB1796 03 45 0C             add         eax,dword ptr [ebp+0Ch]  //将[ebp+0Ch]的值与eax相加
00BB1799 89 45 F8             mov         dword ptr [ebp-8],eax  //将相加的值存入[ebp-8]
	return z;
00BB179C 8B 45 F8             mov         eax,dword ptr [ebp-8]  //把z[ebp-8]中的值放入一个临时的寄存器eax中,将计算结果作为返回值
}

执行到Add函数时也是由esp和ebp开辟的函数栈帧和main函数大同小异

1、将main函数的esp的地址保存在栈中(日后执行pop esp将保存好的esp的地址给回esp)

2、移动esp和ebp

3、将ebx、esi、edi的值保存

4、计算求和

5、将返回值放入eax放入寄存器中返回

函数销毁

00BB179F 5F                   pop         edi  //在栈顶弹出一个值,存放到edi中,esp+4
00BB17A0 5E                   pop         esi  //在栈顶弹出一个值,存放到esi中,esp+4
00BB17A1 5B                   pop         ebx  //在栈顶弹出一个值,存放到ebx中,esp+4
00BB17A2 8B E5                mov         esp,ebp  //esp=ebp
00BB17A4 5D                   pop         ebp  //这里就是保存了main的栈底的地址(ebp)弹出并赋值给ebp,esp+4
00BB17A5 C3                   ret  //执行ret命令弹出栈顶的值,就是call指令下一条指令的地址,esp+4然后继续执行

就会回到call指令的下一条指令的地方继续执行

00BB187D 83 C4 08             add         esp,8  //因为有俩个形参在栈顶所以+8跳过两个形参
00BB1880 89 45 E0             mov         dword ptr [ebp-20h],eax //eax就是计算好返回值,[ebp-20h]就是ret,本次函数的返回值就是通过一个临时的寄存器将值带回来的

这就是Add函数的销毁,main函数的销毁也是一样的,回到invoke_main中

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值