浅谈函数栈帧
Posted 东条希尔薇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈函数栈帧相关的知识,希望对你有一定的参考价值。
作者的码云地址:https://gitee.com/dongtiao-xiewei
后续作者会更新力扣的每日一题系列,原代码会全部上传码云,推荐关注哦~笔芯~
目录
内存管理和函数栈帧
我们知道计算机分配内存时主要分为以下的几个区域
我们也知道,函数是在栈区上开辟空间的。
每一次的函数调用(包括main函数,自定义函数等)都会在栈区上开辟足够大的空间 ,用于本次函数中的内部操作(数据保护,局部变量控制),这块空间我们称为函数栈帧。
这篇文章可以为大家解答以下问题:
- 局部变量怎么创建?
- 为什么局部变量具有随机值?
- 函数怎么传参?
- 形参与实参的关系?
- 函数的调用怎么进行?
- 函数怎么返回值?
由于文章较长,而且涉及反汇编,也就是汇编语言的知识,如果读者想知道答案,可以直接看栈区图和总结与提示板块。
我们的研究方式:
- VS2019
- 编写一段简单的程序
- 在调试过程中查看反汇编过程
正文分界线
正文分界线
一些准备工作
首先在VS2019中敲出以下代码
//test.c
int Add(int x,int y)
{
int z=0;
z=x+y;
return z;
}
int main()
{
int a=0;
int b=0;
int c=0;
c=Add(a,b);
printf("%d\\n",c);
return 0;
}
按F10开始逐过程调试,F11可以逐语句调试(也就是进入函数内部)
然后点击反汇编选项,将会观察到以下代码
这是这个代码全部的反汇编代码
int main()
{
000818B0 push ebp
000818B1 mov ebp,esp
000818B3 sub esp,0E4h
000818B9 push ebx
000818BA push esi
000818BB push edi
000818BC lea edi,[ebp-24h]
000818BF mov ecx,9
000818C4 mov eax,0CCCCCCCCh
000818C9 rep stos dword ptr es:[edi]
000818CB mov ecx,offset _2A160C42_test@c (08C003h)
000818D0 call @__CheckForDebuggerJustMyCode@4 (08131Bh)
int a = 0;
000818D5 mov dword ptr [a],0
int b = 0;
000818DC mov dword ptr [b],0
int c = 0;
000818E3 mov dword ptr [c],0
c = Add(a, b);
000818EA mov eax,dword ptr [b]
000818ED push eax
000818EE mov ecx,dword ptr [a]
000818F1 push ecx
000818F2 call _Add (0810B4h)
000818F7 add esp,8
000818FA mov dword ptr [c],eax
printf("%d\\n", c);
000818FD mov eax,dword ptr [c]
00081900 push eax
00081901 push offset string "%d\\n" (087B30h)
00081906 call _printf (0810D2h)
0008190B add esp,8
return 0;
0008190E xor eax,eax
}
00081910 pop edi
00081911 pop esi
00081912 pop ebx
00081913 add esp,0E4h
00081919 cmp ebp,esp
0008191B call __RTC_CheckEsp (081244h)
00081920 mov esp,ebp
00081922 pop ebp
00081923 ret
这里笔者只讲与本文相关,也就是printf以前的汇编指令。
注意:由于编译器的不同,测试结果可能与本文不同,此文仅做参考~
main函数栈帧
push压栈操作
什么是压栈?出栈?
我们知道栈区的使用规则是:先使用高地址,再使用低地址,回收也是先回收最顶上的地址,也就是低地址,再回收高地址,也就是先进后出。
这样我们先分析以下几行反汇编代码:
000818B0 push ebp
000818B1 mov ebp,esp
000818B3 sub esp,0E4h
哦对了,还没为大家介绍ebp和esp是啥,马上补上~
ebp和esp
首先甩出定义:是计算机中可以存放地址的寄存器,用于维护函数栈帧
那么可能大家又好奇了,啥是维护函数栈帧啊?
我们已经知道,ebp和esp寄存器是用于存放地址的,那么是存放哪里的地址呢?
后文大家可以找到答案,不过还是先把结论甩在这儿吧:
- esp用于存放栈底指针,指向被维护函数最高地址
- ebp用于存放栈顶指针,指向被维护函数最低地址
所以,位于esp和ebp之间的空间,也就是内存为此次函数分配的空间,也就是函数栈帧啦~
解释完了,我们接下来执行这几句命令
main函数空间的开辟
这是执行到mov指令时的esp和ebp的地址
可以知道,第一步push指令将预开辟的main函数的最高地址存放至ebp中,第二步将ebp存放的地址同时存放到esp中
接下来执行这几条指令
000818B3 sub esp,0E4h
000818B9 push ebx
000818BA push esi
000818BB push edi
000818BC lea edi,[ebp-24h]
000818C9 rep stos dword ptr es:[edi]
第一行,我们将esp往上移动,也就是往低地址移动0E4h这么大的字节
第二行,将ebx,esi,edi,分别执行压栈操作
第四行,LEA(load effective address)读取此时esp的地址,为后文做下铺垫
最后一行,将此时ebp和esp之间的空间全部初始化为cc cc cc cc
这里是main函数空间开辟的关键步骤的栈区图
这里也可以顺便解释为什么变量没有初始化是随机值的原因~
不同编译器初始化的值不一样,但VS2019是cc cc cc cc,对应汉字也就是大家熟知的烫烫烫。。。
至此,main函数空间开辟完毕~
变量的创建以及传参
创建变量
由以下几段反汇编代码实现
int a = 0;
000818D5 mov dword ptr [a],0
int b = 0;
000818DC mov dword ptr [b],0
int c = 0;
000818E3 mov dword ptr [c],0
以下是创建变量时的栈区图
传参(形式参数的创建):
先甩结论:形式参数创建在main函数的顶部,并不在函数内部创建
以下几行代码可以解释函数传参
000818EA mov eax,dword ptr [b]
000818ED push eax
000818EE mov ecx,dword ptr [a]
000818F1 push ecx
由于局部变量有先进后出,所以为了确保a能够先被调用,a将比b后创建,这样确保了a会被b先使用,也就是参数是从右向左传递的
这几行代码反应成汉语就是:
将b的值存进eax中并进行压栈操作,再对a进行相同操作
当然,进行了这步操作,为了能维护函数,还需要将ecx最顶端的地址存放在esp中
然后接下来一条指令
000818F2 call _Add (0810B4h)
将add的地址存放在顶部,这次一个伏笔,后文会提到~
进入函数内部
这是函数内部的反汇编代码
int Add(int x, int y)
{
00081770 push ebp
00081771 mov ebp,esp
00081773 sub esp,0CCh
00081779 push ebx
0008177A push esi
0008177B push edi
0008177C lea edi,[ebp-0Ch]
0008177F mov ecx,3
00081784 mov eax,0CCCCCCCCh
00081789 rep stos dword ptr es:[edi]
0008178B mov ecx,offset _2A160C42_test@c (08C003h)
00081790 call @__CheckForDebuggerJustMyCode@4 (08131Bh)
int z = 0;
00081795 mov dword ptr [z],0
z = x + y;
0008179C mov eax,dword ptr [x]
0008179F add eax,dword ptr [y]
000817A2 mov dword ptr [z],eax
return z;
000817A5 mov eax,dword ptr [z]
}
000817A8 pop edi
000817A9 pop esi
000817AA pop ebx
000817AB add esp,0CCh
000817B1 cmp ebp,esp
000817B3 call __RTC_CheckEsp (081244h)
000817B8 mov esp,ebp
000817BA pop ebp
000817BB ret
Add函数栈帧开辟
这段代码与main函数同理,为Add开辟函数栈帧
00081770 push ebp
00081771 mov ebp,esp
00081773 sub esp,0CCh
00081779 push ebx
0008177A push esi
0008177B push edi
0008177C lea edi,[ebp-0Ch]
0008177F mov ecx,3
00081784 mov eax,0CCCCCCCCh
00081789 rep stos dword ptr es:[edi]
0008178B mov ecx,offset _2A160C42_test@c (08C003h)
00081790 call @__CheckForDebuggerJustMyCode@4 (08131Bh)
参数传递与返回
int z = 0;
00081795 mov dword ptr [z],0
z = x + y;
0008179C mov eax,dword ptr [x]
0008179F add eax,dword ptr [y]
000817A2 mov dword ptr [z],eax
return z;
000817A5 mov eax,dword ptr [z]
由这几段的代码得出结论,计算可以直接在eax,ecx,也就是临时变量上进行
最后将得出的结果放入eax中
000817A8 pop edi
000817A9 pop esi
000817AA pop ebx
000817AB add esp,0CCh
000817B1 cmp ebp,esp
000817B3 call __RTC_CheckEsp (081244h)
000817B8 mov esp,ebp
000817BA pop ebp
000817BB ret
1-3行代码涉及将最顶上三个元素弹出
而其中mov esp,ebp(将ebp的地址存储到esp地址中)这条指令,成功实现了将Add函数栈帧回收
pop ebp 指令将ebp弹入原main最下部,可以继续维护原main函数
而最后的ret指令,可以实现回到main函数中call指令发出的地方,也就是存放地址的地方,可以实现整个程序能够继续按照流程继续下去。
重新回到main函数
000818F7 add esp,8
000818FA mov dword ptr [c],eax
第一步,将原来为临时变量x,y开辟的空间回收
第二步,将eax中存放的z的值传给c
至此,成功实现z的返回
总结与提示
- 局部变量在main函数栈帧中创建,数据类型决定了内存修改权限大小
- 不同的编译器初始化放进的值不一样,从而导致了不初始化将会出现随机值的情况
- 函数传参是在main最顶上执行压栈操作,从右到左传参,并没有创建在函数内部
- 形参是实参的一份临时拷贝
- 函数执行必须先创建函数栈帧,再进行函数操作
- 函数若要返回值,需要先将返回值暂时存在寄存器中,方便函数空间被回收后返回
本期内容到此结束啦!由于作者水平有限,文章中如有不足与疏漏之处在所难免,希望各位大佬提出你们宝贵的意见!笔芯~
以上是关于浅谈函数栈帧的主要内容,如果未能解决你的问题,请参考以下文章