(史上最详细的解释看过来)深入理解函数栈帧

函数的调用过程(栈帧)

话说,什么是函数栈帧?我之前也是一脸懵逼的(┭┮﹏┭┮),举个栗子,先看一段简单的代码:

#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 ret = Add(a, b);
	printf("ret=%d\n", ret);
	return 0;
}

这段代码我想应该没人看不懂的,那么我们想知道如何调用函数的,程序调试的时候,查看【调用堆栈】,如下图:
这里写图片描述

我们发现,其实main函数在mainCRTStartup()函数中调用的  
  提到栈帧的维护我们就必须了解ebp和esp这两个寄存器。在函数调用过程中这两个寄存器放了维护这个栈的栈底和栈顶指针。

例如,在调用main函数是,我们为main函数分配栈帧空间,那么栈帧维护如下:这里写图片描述

ebp存放了指向函数栈帧栈底的地址
esp存放了指向函数栈帧栈顶的地址

接下来我们具体分析上面一段代码:

上面我们知道,main函数是被调用过来的,那么这个程序第一步会为main函数分配栈空间
调试并转到反汇编我们会看到下面一段一眼望去并不怎么懂得代码: ![这里写图片描述](https://img-blog.csdn.net/201807251957445?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNTUwMDE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
在这里我简单解释一下上图:
002B17F0  push        ebp  
002B17F1  mov         ebp,esp  
002B17F3  sub         esp,0E4h  
002B17F9  push        ebx  
002B17FA  push        esi  
002B17FB  push        edi  
第一步:将ebp压栈
第二步:将esp赋给ebp
第三步:是给esp减去0E4h的大小
接下来几步:分别将ebx,esi,edi压栈
我们知道,一开始ebp存放了指向mainCRTStartup函数栈帧栈底的地址,那么第一步将ebp压栈就会在原来esp上面多了一块空间,而这块空间此时就是ebp压栈进去的,然后将esp指向的地址给ebp,再给esp减去0E4h,即将esp上移了,图解如下:

这里写图片描述
从上图中我们很容易的看出在调用main函数是为main函数开辟的栈空间即栈帧。

那么紧接着,又将ebx,esi,edi压栈,会出现如下图所示:

这里写图片描述

002B17FC  lea         edi,[ebp-0E4h]  
002B1802  mov         ecx,39h  
002B1807  mov         eax,0CCCCCCCCh  
002B180C  rep stos    dword ptr es:[edi]  
接下来这四句,lea是加载有效地址,从上图中,我们知道ebp指向的地址,那么edi存放的就是ebp-0E4h的地址也就是③esp处的地址,最后一句rep stos dword ptr es:[edi] 意思是从edi里面重复拷贝ecx次eax的内容。

这里写图片描述
接下来的汇编代码如下:

int a = 10;
002B180E  mov         dword ptr [a],0Ah  
	int b = 20;
002B1815  mov         dword ptr [b],14h  
002B180E   mov  dword ptr [a],0Ah  
这一行其实就是将10移动到0Ah的位置上,其实就是ebp-8
002B1815  mov  dword ptr [b],14h
这一行和上面一行意思是一样的,将20放在14h的位置上,就是ebp-20
注:emmmmmmm这里有必要解释一下为什么是ebp-8和ebp-20,因为自己刚开始就非常的不解,其实在定义a的时候就将a压栈进去,那么ebp就要上移,即ebp要减去一个数,至于减多少,这个是跟编译器有关的,我们可以内存查一下ebp和&a很容看出这两者之间相差了8,图如下:

这里写图片描述
这里写图片描述
接着就调用Add函数了:

int ret = Add(a, b);
002B181C  mov         eax,dword ptr [b]  
	int ret = Add(a, b);
002B181F  push        eax  
002B1820  mov         ecx,dword ptr [a]  
002B1823  push        ecx  
这段就比较容易懂了,仔细看过之后就会知道,其实是将a和b压栈,用ecx和eax来接收a和b,起到形参作用

这里写图片描述

002B1824  call        _Add (02B1109h)  
002B1829  add         esp,8  
002B182C  mov         dword ptr [ret],eax 
接下来就是调用Add函数了,我们具体转到Add函数的反汇编分析一下:
(未截完)

这里写图片描述
我们还是和之前一样分块分析

002B16E0  push        ebp  
002B16E1  mov         ebp,esp  
002B16E3  sub         esp,0CCh  
002B16E9  push        ebx  
002B16EA  push        esi  
002B16EB  push        edi  
002B16EC  lea         edi,[ebp-0CCh]  
002B16F2  mov         ecx,33h  
002B16F7  mov         eax,0CCCCCCCCh  
002B16FC  rep stos    dword ptr es:[edi]  
	int z = 0;
002B16FE  mov         dword ptr [z],0  
这里Add函数创建栈帧的过程其实和main函数一样的,先将ebp压栈,再将esp的地址存放到ebp里面,此时,esp和ebp指向同一位置,再将esp上移0CCh个位置,然后就是ebx,esi,edi压栈,这里不多作说明,详细图解如下:

这里写图片描述

z = x + y;
002B1705  mov         eax,dword ptr [x]  
002B1708  add         eax,dword ptr [y]  
002B170B  mov         dword ptr [z],eax  
return z;
002B170E  mov         eax,dword ptr [z]  
这里就更容易懂了,eax存放x的值,再给这个值加上y的值,再把这个值赋给z,最后将z的值返回给eax
002B1711  pop         edi  
002B1712  pop         esi  
002B1713  pop         ebx  
002B1714  mov         esp,ebp  
002B1716  pop         ebp  
002B1717  ret  
这一步,显然我们看到pop,出栈了,就是销毁栈帧的过程,因为这个时候,Add函数已经用完了,将edi,esi,ebx按顺序出栈,这时,我们要将ebp出栈,就得将esp指到ebp的位置,这时esp和ebp指向同一位置,这个位置就是我们所保存的main()函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了。 当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main()函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。

这里写图片描述

到这里我们可以看到销毁Add栈帧出来会指向call指令的下一个指令
002B1829  add         esp,8  
002B182C  mov         dword ptr [ret],eax  
	printf("ret=%d\n", ret);
002B182F  mov         eax,dword ptr [ret]  
002B1832  push        eax  
002B1833  push        offset string "ret=%d\n" (02B7B30h)  
002B1838  call        _printf (02B132Fh)  
002B183D  add         esp,8  
	return 0;
给esp+8之后显然把刚开始在main函数里用来接收a和b的形参也弹了出去,然后把eax(就是计算出来的值)放到ret里。再把ret给eax,将eax压栈,offset是字符串的偏移地址,再调用printf函数。 最后销毁main函数栈帧和上面销毁Add函数栈帧是一个意思,这里不多做说明。
以上是我个人的理解,如有纰漏和错误的地方,还请各方大神指正,感激不尽!
  • 22
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值