程序员的内功修炼——深入理解函数栈帧


1. 写过无数代码的你是否想过这些问题?

1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.函数是怎么传参的?传参的顺序是怎么样的?
4.形参和实参是什么样的关系?
5.函数调用是怎么做的?
6.函数调用之后结束是怎么返回的?

在这里插入图片描述

看到这些题目是否觉得自己并没有掌握函数的调用等等一系列的问题?不用着急 ,接下来我们慢慢探索。那我们该怎么去了解呢?我们这次使用的编译环境是VS2013。为什么使用VS2013呢?因为越高级的编译器越不容易观察到函数的栈帧,而且不同的编译器,函数调用过程中栈帧的创建也是略有差异的。那接下来就进入正题:

2.了解寄存器和大概轮廓

我们的寄存器有许多 ,例如 eax,ebx,ecx,edx。还有ebp,esp,今天我们重点观察的就是ebp寄存器和esp寄存器。

ebp寄存器和esp寄存器中存放的是地址,这两个地址是维护函数栈帧的。edp是栈底的指针,即指向的是函数开辟空间的底部的,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\n",c);
	return 0;
}

首先简单的了解,在我们调用函数是实在栈区上开辟空间,我们还需要知道栈区使用的规则是先用高地址再用低地址(即低地址是栈顶),如果低地址有数据等等是不能直接使用高地址的数据的,我们在低地址的地方放数据是压栈的过程,删除低地址的数据是出栈的过程。如下图:

在这里插入图片描述
那你知道其实main函数也是被调用的函数吗?
将上面的代码在VS2013上进行调试(ctrl+F10)然后再点击调试---->窗口---->调用堆栈。
在这里插入图片描述
然后再f10 知道代码结束就能看到这个:
在这里插入图片描述
我们就可以知道main函数其实也是被调用的函数,它也是被__tmainCRTStartup函数调用的。
在这里插入图片描述

接下来我们还可以看到,__tmainCRTStartup函数又是被mainCRTStartup调用的。
在这里插入图片描述
总结画图就是这样的:
在这里插入图片描述
所以在刚刚画图的过程当中我在main函数的下面还空了一部分空间,其实就是这些函数的空间。

观察汇编代码

接下来就是真正进入了主题,根据汇编代码来分析函数栈帧到底是怎么一回事。

我们开始调试(Ctrl+f10)然后右键鼠标,点击转到反汇编。

在这里插入图片描述
在这里插入图片描述

1.分析main函数的汇编代码:

看到代码是不是有点慌张,不要慌张,我会一条一条的解释的。
在这里插入图片描述

我们进入main函数之前函数的堆栈应该是这样的
在这里插入图片描述

第一条代码(push):

00553B40  push        ebp  

第一条:push 用英文翻译就是压栈的意思,其实就是在栈顶上放一个元素。
就是在栈顶上放一个ebp的寄存器的地址。这个时候我们需要观察一下地址的变化,我们要记住的就是esp寄存器始终是指向栈顶元素的,不管是push(压栈),还是pop(出栈),esp的地址都会发生改变的。我们调试是时候打开窗口----->内存----->内存1(随便一个都可以)。
在这里插入图片描述
首先观察esp最初的地址(操作如图所示)
在这里插入图片描述

然后打开监视:
在这里插入图片描述
输入esp
然后F10就能执行汇编代码的第一条,也就是刚刚那条,就可以看到,esp的值变小了4,就代表在栈顶压入了一个元素。
画图就是这样:
在这里插入图片描述

第二条代码(mov):

00553B41  mov         ebp,esp 

mov的意思就是将esp的地址赋给ebp

执行代码之前:
在这里插入图片描述
执行代码之后:
在这里插入图片描述
图形的变化就是:
在这里插入图片描述

第三条代码(sub):

00553B43  sub         esp,0E4h  

sub的意思就是将esp地址减去一个0E4h,那0E4h是多少呢?
这张图也是执行代码之前的esp的地址。
在这里插入图片描述
先取消16进制显示在监视中输入0E4h,可以看到值是228。
在这里插入图片描述
执行代码后esp的变化:
在这里插入图片描述
画图可以看到是这样的变化:
在这里插入图片描述
其实几条代码就是为了给main函数开辟

在这里插入图片描述

第四五六条代码(push):

00553B49  push        ebx  
00553B4A  push        esi  
00553B4B  push        edi

三条都是一样的push上个寄存器在上面:
这个跟前面一样,我就只画图:
在这里插入图片描述

esp的地址变化就是:从0x00aff9f0---->0x00aff9ec---->0x00aff9e8---->0x00aff9e4
在这里插入图片描述
在这里插入图片描述

第七八九十条代码(lea指令)

00553B4C  lea         edi,[ebp-0E4h]  
00553B52  mov         ecx,39h  
00553B57  mov         eax,0CCCCCCCCh  
00553B5C  rep stos    dword ptr es:[edi] 

这几条代码的意思就是将edi开始向下的39h个dword(double word,4个字节)的值改成0cccccccch。
edi的地址是什么呢?是ebp-0E4h(这个就很棒了),ebp - 0E4h就是main函数栈帧的栈顶。
我们直接看图:
在这里插入图片描述
一直到ebp的地址才结束:
在这里插入图片描述
我们画图:
在这里插入图片描述

第十一条代码(mov)

前面这么久都没有进行一条代码,接下来才是执行代码。

	int a = 10;
00553B5E  mov         dword ptr [ebp-8],0Ah  

就是将0Ah的值放进ebp-8的地址当中。
在这里插入图片描述
画图:
在这里插入图片描述
其实就是将ebp的上面上面第二个地址放入了a = 10;

第十二十三条代码(mov)

	int b = 20;
00553B65  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00553B6C  mov         dword ptr [ebp-20h],0 

和上面一样,将b = 20,c = 0 放在ebp - 14h和ebp - 20h的位置。
这里我们就能知道,如果在main函数中变量不初始化就是随机值,就是0xffffffff。
走到这里,我们的main函数才结束。
看图:
在这里插入图片描述

Add函数汇编代码的解析

先看代码:

	c = Add(a, b);
00553B73  mov         eax,dword ptr [ebp-14h]  
00553B76  push        eax  
00553B77  mov         ecx,dword ptr [ebp-8]  
00553B7A  push        ecx  
00553B7B  call        005511E0  
00553B80  add         esp,8  
00553B83  mov         dword ptr [ebp-20h],eax

第一二三四条代码(mov和push)

00553B73  mov         eax,dword ptr [ebp-14h]  
00553B76  push        eax  
00553B77  mov         ecx,dword ptr [ebp-8]  
00553B7A  push        ecx  

mov我们也知道了,一二条是将ebp - 14h地址的值放入eax寄存器中并将eax压入栈区,三四条也是将ebp - 8地址的值放入寄存器ecx中并将其压入栈顶。这个过程就是函数的传参。
我们画图:
在这里插入图片描述
这里我们就是清晰知道,函数形参就是实参的临时的一份拷贝,我们知道,改变形参不会改变实参,这个想法是完全正确的。

第五条代码(重要,call指令)

00553B7B  call        005511E0  

这个时候就是要讲call下一条指令压栈压入了栈顶。

画图:
在这里插入图片描述
看图顶部地址就是call指令的下一条地址。那为什么要记住这个地址呢?

因为我们接下来就进入到了Add函数当中了,我们进入Add函数之后总要回来吧,这个时候就是方便函数调用完毕后回来。

接下来就要按F11真正进入到了Add函数当中:

int Add(int x, int y)
{
00553CD0  push        ebp  
00553CD1  mov         ebp,esp  
00553CD3  sub         esp,0CCh  
00553CD9  push        ebx  
00553CDA  push        esi  
00553CDB  push        edi  
00553CDC  lea         edi,[ebp+FFFFFF34h]  
00553CE2  mov         ecx,33h  
00553CE7  mov         eax,0CCCCCCCCh  
00553CEC  rep stos    dword ptr es:[edi]  
	int z = 0;
00553CEE  mov         dword ptr [ebp-8],0  
	z = x + y;
00553CF5  mov         eax,dword ptr [ebp+8]  
00553CF8  add         eax,dword ptr [ebp+0Ch]  
00553CFB  mov         dword ptr [ebp-8],eax  
	return z;
00553CFE  mov         eax,dword ptr [ebp-8]  
}

这个时候我们再看代码是不是就轻松多了?前面的几行代码和main函数中的很像,其实这就是在为Add函数创建函数的栈帧:

00553CD0  push        ebp  
00553CD1  mov         ebp,esp  
00553CD3  sub         esp,0CCh  
00553CD9  push        ebx  
00553CDA  push        esi  
00553CDB  push        edi  
00553CDC  lea         edi,[ebp+FFFFFF34h]  
00553CE2  mov         ecx,33h  
00553CE7  mov         eax,0CCCCCCCCh  
00553CEC  rep stos    dword ptr es:[edi]  

画图:
在这里插入图片描述

	int z = 0;
00553CEE  mov         dword ptr [ebp-8],0  
	z = x + y;
00553CF5  mov         eax,dword ptr [ebp+8]  
00553CF8  add         eax,dword ptr [ebp+0Ch]  
00553CFB  mov         dword ptr [ebp-8],eax 
	return z;
00553CFE  mov         eax,dword ptr [ebp-8]  

接下来的代码就是先将z的值放入Add函数当中。再将ebp+8的值放入寄存器eax中,再将ebp+0ch的值加入到函数eax中,那ebp+8和ebp+0ch的值是多少呢?

通过上面的图我们清晰的知道,分别是20 ,10,也就是找到了形参的位置并将其加起来放入到了寄存在eax中,现在eax的值就变成了30。最后一条指令就是将eax中的值放入ebp - 8 中,而ebp - 8 的值就是 z 所在的位置。我们要清楚的就是,eax寄存器不会因函数栈帧的摧毁消失而消失,它会一直存在。

00553D01  pop         edi  
00553D02  pop         esi  
00553D03  pop         ebx  
00553D04  mov         esp,ebp  
00553D06  pop         ebp  
00553D07  ret  

接下来的练习的三个pop就是,出栈将栈顶三个元素直接弹出。然后再将ebp的地址给esp,在弹出ebp,我们画图来看:
在这里插入图片描述
有人就会问为啥ebp就跑到下面去了呢?
因为我们开始push了一下ebp地址,然后我们直接pop弹出ebp的时候,ebp就直接找到了main函数的栈顶,然后esp就顺势的指向了开始我们留下来的那个地址。

现在的栈顶有一个call指令的下一条指令,我们ret这个地址的时候,我们就是直接走到了call指令的下一条指令了。
在这里插入图片描述
所以在原先在栈顶push一个call指令的下一条指令的地址,就是为了让我们的代码得到连续。就是让我们的代码不仅走得出去,还能走得回来。

最后两条代码(add和mov)

00553B80  add         esp,8  
00553B83  mov         dword ptr [ebp-20h],eax  

先将esp的地址加上8,再将eax的值放入ebp - 20h(c)中。
我们所有的汇编代码就完了,最后将c中的值打印出来(也是一个函数,有兴趣的朋友可以自己去看汇编代码)。

最后的图:
在这里插入图片描述
至于后面main函数的释放,有兴趣的朋友可以试试。

最后谢谢大家,有错误希望大家及时指正!!一起加油!!!!

  • 23
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值