目录
一.什么是函数栈帧?
函数在c语言编程里面是一个具有单独功能不在main主函数中的一个独立存在,我们把它抽象的理解为函数,可以说c语言的程序实现是由一个个函数组成的。那什么是函数栈帧,其实就是c语言为调用函数时在程序调用栈中单独开辟的空间。这些空间里面存放着函数的形参,实参,函数返回值,变量以及esp,ebp等。
二.理解函数栈帧的创建能解决哪些问题?
在理解之前,在写函数的时候会有一些问题,比如:
函数的实参是怎么传给形参的?
传参的顺序是和函数实参顺序一致吗?
函数运行后返回的最终值是怎么传回到main函数的?
为什么没初始化的局部变量是随机的?
形参和实参是什么关系?
函数调用结束怎么返回的?
局部变量是怎么创建的?
......
在理解函数栈帧的创建与销毁后,这些问题可能会烟消云散。一起看看
三.创建函数栈帧空间的之前认知
3.1 什么是栈
栈,应该都有所耳闻,程序员都曾听到全栈,函数栈的概念,栈,一种数据结构,可以将数据压栈,入栈(push),也可以将数据弹出(pop)栈。
数据的出入规则是:先入栈的数据后出 栈( FIFO)。就像叠成一叠的书本放在一个箱子里,先叠上去的书在最下面,因此要最后才能取出。不能从底下去抽出来。 在计算机中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在windows系统中,栈总是向下增长(由高地址向低地址)的。即最下面为高地址,最上面是低地址。
3.2认识相关寄存器
在栈帧空间中,这些寄存器是需要用的,需要提前了解,才能更好理解栈帧的运行和维护
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
3.3 汇编指令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用
1. 压入返回地址 2. 转入目标函数执行
ret:恢复返回地址,压入eip。
知道以上寄存器和指令(术语)才能帮助我们更好理解空间的每一步运行过程。
四.创建和销毁全过程
4.1预备知识
在这之前,我们需要了解一些前提或者是认知才能理解函数栈帧空间的创建和销毁,ebp和esp是维护当前esp和ebp所在空间的栈顶和栈底,这是两个寄存器的“使命”,每次调用函数,都会为这次函数调用开辟空间,为函数栈帧的空间,如图所示
4.1.1调用堆栈
用一个典型的代码案例来演示
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
调试进入Add函数后,我们就可以观察到函数的调用堆栈
是不是发现main函数的调用也是由其他函数__tmainCRTStartup调用的呢
_tmainCRTStartup也是被下面那个mainCRTStartup调用的。
在调用add函数之前,首先调用的main函数,main也是有属于自己的栈帧空间才对
4.2打开反汇编
图片演示
鼠标右击空白处,转到反汇编
去掉符号名,然后图中箭头所指的【a】就会不见,我们需要的是地址,不是字符
4.3函数栈帧创建
int main()
{
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
int a = 3;
00BE183B mov dword ptr [ebp-8],3
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0
ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
printf("%d\n", ret);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
return 0;
00BE1874 xor eax,eax
}
以上是main函数转化为汇编的所有代码,那么接下来干嘛?拆
注:vs每个编译器执行f10调试的时候会重新分配内存,所以每次汇编代码地址是有差异的。这个是没有什么影响的。
在没有执行进入main函数的第一个push压栈指令之前,esp和ebp的地址是这样的
esp:0x008ffba8
ebp:0x008ffbf4
push ebp,此时esp的地址应该是往上走,因为你把ebp压进去了,而地址从上往下是由低到高,所以往上是低地址,是减。
a8-a4,是不是减了4
而在内存中,可以看到ebp确实被压进去了(监视器里看内存变化)
看之前的代码,下一个指令是mov ebp,esp,把esp的值转移给ebp
把栈顶的值给栈底,即ebp要往上走代替esp的值(为main函数开辟空间做准备了)
sub esp,0E4h,给esp减去0E4h,转化为16进制是228,就是把esp减去那么多个字节,减就是往上走
地址变化了
图例:
这个时候esp在最上面
三个push ebx esi edi每压栈一次,esp就会往上走一次,共走三次
注意 ,这时esp的值应该是-12,压栈一次减4个字节
lea edi,[ebp-0E4h],加载有效地址,将edi的地址变成ebp-0E4h
mov ecx ,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
39h存放在ecx里,0CCCCCCCCh存放在eax寄存器里,dword是一个word两个字节,double倍
这前三个行指令意思把edi位置往下的ecx次(转化为10进制就是57),57次,每一次都是double word,就是一次传4个字节,全部变成CCCCCCCC给到eax,到ebp往上的所有内容57*4个字节
以上都是为main函数栈帧的开辟
变量a的创建,在ebp-8的位置放入3,一个格子代表一个字节
变量b的创建,在ebp-14h地址处放入5
要是没有把a,b变量初始化,就是随机值cccccccccc,“烫烫烫烫烫烫烫烫烫烫”
变量c创建同理
整体效果是这样的:
接下来调用add函数,传参就是把实参数的值push到栈帧空间
mov eax ,dword ptr [ebp-14h],这个ebp-14h是b,把b的值放到eax寄存器里
push!在栈顶上压栈,压的是eax(实参b),之前esp的值是0x008ffaB4,把14压在了esp上
同理,mov ecx,dword ptr [ebp-8],[ebp-8]是a,把a的值放入ecx寄存器里
push!在eax上压栈,压的是ecx(实参a),esp同步往上移位
call,函数调用,记住这个call之前的00BE1858,后面有用
call指令的作用是将call指令下一条指令的地址入栈,是因为call执行时会进入Add指令的内部,在Add指令走完之后会回到主函数中,按F11走进Add
红线标的那条地址入栈,压在ecx上,这个地址是需要记住主函数中指令走到哪里的位置,因此需要将下一步指令的地址入栈,才能回来,这里逻辑是很严密的。
F11走进去,来到Add函数中
int Add(int x, int y)
{
00BE1760 push ebp
00BE1761 mov ebp,esp
00BE1763 sub esp,0CCh
00BE1769 push ebx
00BE176A push esi
00BE176B push edi
int z = 0;
00BE176C mov dword ptr [ebp-8],0
z = x + y;
00BE1773 mov eax,dword ptr [ebp+8]
00BE1776 add eax,dword ptr [ebp+0Ch]
00BE1779 mov dword ptr [ebp-8],eax
return z;
00BE177C mov eax,dword ptr [ebp-8]
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp,ebp
00BE1784 pop ebp
00BE1785 ret
这里其实和main函数的栈帧空间创建原理一样
push ebp 压栈
原本的ebp是在维护main函数的,现在要压main的ebp上来,esp地址变小,然后mov esp 给ebp,赋值给add的ebp
下一步,同样是sub,把esp减去0CCh个值,给add函数创造栈帧空间
这里是重新计算add里esp和ebp的位置
执行3个push,将ebx,esi,edi入栈,esp继续向上走,最后形成空间
z临时变量的创建,把0放入ebp-8的位置上
注意下一步的操作,
eax,dword ptr [ebp+8],是把ebp+8的值放在eax里,是在找回原来压栈a的参数
eax,dword ptr [ebp+0Ch],把ebp+0Ch加给eax,就是把原来压栈b的参数给eax,形成a+b=8
最后,命令eax的值放入ebp-8,意思是把a+b的值赋值给z!
我们在调用add函数的时候,就已经铜鼓传参的形式把a,b的值传到add里并且push入栈了,所以说形参是实参的一份临时拷贝是完全正确的。
接下来return z
这里很奇妙,因为返回就意味着销毁,所以是把z的值ebp-8暂时放在寄存器里来进行返回,add销毁但不意味着寄存器eax销毁
4.4函数栈帧销毁
接下来执行三个连续的pop
pop edi pop esi pop ebx,弹出,此时esp会加12,每次弹出加4
mov esp,ebp 再将ebp的值给esp,esp继续往下,回收add栈帧空间
pop ebp 弹出ebp,此时栈顶刚好指的是main函数的ebp,弹出后ebp就会回到原来的main函数里。(ebp重新开始维护main的栈底,esp重新开始维护栈顶)
ret指令的执行,之前我们就已经把call指令的下一条指令入栈了,pop ebp后此时栈顶的值刚好是指令的地址,然后直接跳转到主函数call指令下一条指令的地址处,继续往下执行。
在ret执行结束之后弹出栈顶元素,Add函数栈帧销毁了。
回到main函数的时候,可以看到:
add esp,8,add函数都调用完了,地址已经弹出了,esp+8,继续往下
把eax移动到ebp-20h,就是把之前存放在eax的值给ebp-20h,等于存放到main的ret,eax保存的数据是add函数x+y的值,它是由寄存器带回来的,所以从里面读取出来。
最后,我们需要销毁main函数的栈帧,道理和Add函数栈帧的销毁是一样的,这里就不过多解释了。
那么来回答一下二的问题
1.局部变量是怎么创建的?
答:是函数分配好栈帧空间后,初始化了我们的空间内部cccccc值,然后给我们的变量对应的分配内存
2.函数的实参是怎么传给形参的?
答:是在没调用Add函数时(没有call),通过实参传给寄存器然后从右到左push到Add的栈帧空间,在Add函数里,是通过指针偏移量ebp+8,ebp+12找到了原本已经压栈的a,b值。
3.传参的顺序是和函数实参顺序一致吗?
答:不一致,本案例是先b再a push进Add函数
4.函数运行后返回的最终值是怎么传回到main函数的?
答:通过寄存器eax返回
5.为什么没初始化的局部变量是随机的?
答:没初始化前,变量都是随机放入的,不初始化,就没有给局部变量分配内存,当然是随机的。
6.形参和实参是什么关系?
答:形参是在压入栈中的一片空间,他只是拷贝了实参的值,空间是独立的,改变形参不会影响实参。
7.函数调用结束怎么返回的?
答:把call指令的下一条指令记录入栈了,然后跳转到函数中去,先将实参拷贝压入栈中,再将调用这个函数的ebp压入栈中,给函数开辟空间。
函数调用结束,弹出ebp,这个ebp刚好为栈顶main函数的epb,弹出,ret指令首先从栈顶弹出一个值,此时栈顶的值就是是call指令的下一个地址,弹出后可以直接跳转到主函数call指令的下一个地址,可以继续执行。
ok,结束,感谢观看!
标签:销毁,函数,esp,mov,eax,详解,ebp,全过程,push From: https://blog.csdn.net/2301_76684563/article/details/143623827