C语言|【C终章】函数栈帧的创建和销毁

目录
一、本文目标
二、基础知识
1、寄存器
2、代码案例
3、总体栈帧概况
4、所需反汇编代码总览
三、函数栈帧创建销毁过程
1、_tmainCRTStartup函数(调用main函数)栈帧的创建
2、main函数栈帧的创建
3、main函数内执行有效代码(变量)
4、Add函数栈帧的创建
5、Add函数内执行有效代码
6、Add函数栈帧的销毁
7、main函数栈帧的销毁
四、总结
一、本文目标

  • 1、局部变量是怎么创建的?
  • 2、为什么局部变量的值是随机值?
  • 3、函数是怎么传参的?传参的顺序是怎样的?
  • 4、形参和实参是什么关系?
  • 5、函数调用是怎么做的?
  • 6、函数调用结束后是怎么返回的?
当我们深入理解函数栈帧创建和销毁,答案自然就清楚了。正文开始:
二、基础知识
1、寄存器
寄存器名称 简介
eax "累加器"它是很多加法乘法指令的缺省寄存器。
ebx "基地址"寄存器, 在内存寻址时存放基地址。
ecx 计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。
edx 总是被用来放整数除法产生的余数。
esi 源索引寄存器
edi 目标索引寄存器
ebp (栈底指针)"基址指针",存放的是地址,用来维护函数栈帧
esp (栈顶指针)专门用作堆栈指针,存放的是地址,用来维护函数栈帧
2、代码案例
  • 本文依赖的编译器:VS2013
#include 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; }

3、总体栈帧概况
每一个函数调用都要在栈区为它开辟空间,像上述的代码中,有肉眼可见的main函数和Add函数,相应的需要为它俩开辟空间,但其实main函数也是被调用的,当我们针对上述代码按下F10,按到return 0时再按一次,就会跳出以下界面
C语言|【C终章】函数栈帧的创建和销毁
文章图片

由图得知,main函数是被__tmainCRTStartup函数调用的,而 __tmainCRTStartup又是被mainCRTStartup调用的。先看下总体函数栈帧开辟情况:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

  • 两个重要知识点:
  1. 压栈(push):给栈顶放一个元素
  2. 出栈(pop):从栈顶删除一个元素
接下来会详细讲解下函数栈帧的开辟情况:
4、所需反汇编代码总览
int main() { 00031410pushebp 00031411movebp,esp 00031413subesp,0E4h 00031419pushebx 0003141Apushesi 0003141Bpushedi 0003141Cleaedi,[ebp+FFFFFF1Ch] 00031422movecx,39h 00031427moveax,0CCCCCCCCh 0003142Crep stosdword ptr es:[edi] int a = 10; 0003142Emovdword ptr [ebp-8],0Ah int b = 20; 00031435movdword ptr [ebp-14h],14h int c = 0; 0003143Cmovdword ptr [ebp-20h],0c=Add(a,b); 00031443moveax,dword ptr [ebp-14h] 00031446pusheax 00031447movecx,dword ptr [ebp-8] 0003144Apushecx 0003144Bcall00C210E1 00031440addesp,8 00031443movdword ptr [ebp-20h],eax printf("%d", c); 00241456movesi,esp 00241458moveax,dword ptr [ebp-20h] 0024145Bpusheax 0024145Cpush245858h 00241461calldword ptr ds:[00249114h] 00241467addesp,8 0024146Acmpesi,esp 0024146Ccall0024113B return 0; 00241471xoreax,eax } 00031451popedi 00031452popesi 00031453popebx 00031454addesp,0E4h 0003145Acmpebp,esp 0003145Ccall__RTC_CheckEsp (03113Bh) 00031461movesp,ebp 00031463popebp 00031464ret

int Add(int x, int y) { 000313C0pushebp 000313C1movebp,esp 000313C3subesp,0CCh 000313C9pushebx 000313CApushesi 000313CBpushedi 000313CCleaedi,[ebp-0CCh] 000313D2movecx,33h 000313D7moveax,0CCCCCCCCh 000313DCrep stosdword ptr es:[edi] int z = 0; 000313DEmovdword ptr [ebp-8],0 z = x + y; 000313E5moveax,dword ptr [ebp+8] 000313E8addeax,dword ptr [ebp+0ch] 000313EBmovdword ptr [ebp-8],eax return z; 000313EEmoveax,dword ptr [ebp-8] } 000313F1popedi 000313F2popesi 000313F3popebx 000313F4movesp,ebp 000313F6popebp 000313F7ret

三、函数栈帧创建销毁过程
1、_tmainCRTStartup函数(调用main函数)栈帧的创建
根据上文,我们已经知晓main函数是被_tmainCRTStartup函数所调用的,自然要为它开辟栈帧,这块空间应该由ebp和sep俩寄存器来维护,前提是下面高地址,上面低地址。如图:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此时进入main函数,首先要push进行压栈:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

push ebp就是把ebp压到栈顶上,此时sep相应的移动到新栈顶上,可以通过监视来验证:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

C语言|【C终章】函数栈帧的创建和销毁
文章图片
图示如下:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接下来执行mov操作:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此行代码意思就是把sep赋给ebp,所以ebp指向的位置即为sep所指向的位置,但是源操作地址位置不变,可通过监视来验证
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接着执行sub操作:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

该操作就是给esp减去个0E4h ,此时esp的位置就要往上面去,通过监视观察:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此时此刻执行完sub操作,其实就已经进入到下文的main函数栈帧的开辟,至此_tmainCRTStartup函数栈帧的开辟已完成。图示见下文:
2、main函数栈帧的创建
接上文,图示如下:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接下来进行三次push操作: 把ebx、sei、edi顺次压栈压进去,相应的esp也要往上走。
C语言|【C终章】函数栈帧的创建和销毁
文章图片

通过监视看看:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

图示如下:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接下来执行下列三个步骤
C语言|【C终章】函数栈帧的创建和销毁
文章图片
操作lea(load effecitve address)加载有效地址。就是相当于把[ebp+FFFFFF1Ch]放到edi里头,显示符号名后[ebp+FFFFFF1Ch]就是[ebp-0E4h],前面已经执行过-0E4h,这里再执行一次放到edi里头去。接着mov把39h放到ecx里头去,再mov此时eax放的就是0CCCCCCCCh
C语言|【C终章】函数栈帧的创建和销毁
文章图片

上述操作执行后的目的就是从刚才的edi开始向下的39h次这么多个dword(1个word2字节,2dword4个字节)全部改为0CCCCCCCCh
通过监视看下:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

图示如下:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

至此,main栈帧的开辟已经完成,接下来就要执行正式有效代码,见下文:
3、main函数内执行有效代码(变量)
接下来执行以下操作:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

先mov把0Ah(10)放到ebp-8的位置上,同理把14h(20)放到ebp-14h上,把0放到ebp-20h上,如图:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此时此刻a、b、c这三个变量均已创建完成,接下来进行Add函数调用:先进行传参
C语言|【C终章】函数栈帧的创建和销毁
文章图片

首先,mov把ebp-14h(b=20)放到eax里头。接下来再push, 压栈把eax(20)放到栈顶,相应esp也要移动,同理mov把ebp-8(a=10)放到ecx里头,再push把ecx放到栈顶。如图所示:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

C语言|【C终章】函数栈帧的创建和销毁
文章图片

接着执行call操作,调用Add函数,按F10执行到call时,按下F11,此时就跳到Add函数内部并且把call指令的下一条指令的地址压到栈顶。这么做的目的是在接下来跳到Add函数里去回来时方便回到该地址,如图:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

按下F11,此时就正式进入Add函数内部 并为其开辟栈帧,详情见下文:
4、Add函数栈帧的创建
int Add(int x, int y) { 000313C0pushebp 000313C1movebp,esp 000313C3subesp,0CCh 000313C9pushebx 000313CApushesi 000313CBpushedi 000313CCleaedi,[ebp-0CCh] 000313D2movecx,33h 000313D7moveax,0CCCCCCCCh 000313DCrep stosdword ptr es:[edi]

而前面这些操作跟先前main函数内部操作一样,其实就是在为Add函数准备我们的栈帧
首先,push ebp把ebp压栈到栈顶,再mov把esp赋给ebp,再sub,把esp-去0CCh,此步骤就是在为Add函数开辟空间,接着进行三次push,同main函数那样,同理,依旧是初始化成CCCCCCCC,详细过程不再赘述,跟上文main函数一样,如图所示:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

至此,Add栈帧的开辟已基本完成,接下来就要执行正式有效代码,见下文:
5、Add函数内执行有效代码
接上文:
int z = 0; 000313DEmovdword ptr [ebp-8],0 z = x + y; 000313E5moveax,dword ptr [ebp+8] 000313E8addeax,dword ptr [ebp+0ch] 000313EBmovdword ptr [ebp-8],eax return z; 000313EEmoveax,dword ptr [ebp-8] }

【C语言|【C终章】函数栈帧的创建和销毁】首先,把0放到ebp-8的位置上,接着mov把ebp+8的值放到eax里头去,此时eax就是10。再add给eax加上ebp+0ch,就是把20加进去,此时eax就是30,加完后再把eax(30)放到ebp-8里头去,最终的结果(30)放到z里头去。
此时Add函数内部有效代码执行完毕,见图:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接下来就要进行返回了,也就是Add函数栈帧的销毁,见下文:
6、Add函数栈帧的销毁
return z; 000313EEmoveax,dword ptr [ebp-8] } 000313F1popedi 000313F2popesi 000313F3popebx 000313F4movesp,ebp 000313F6popebp 000313F7ret

上文已经知道此时已经把ebp-8的值(30)放到eax里头去,接下来执行三次pop,一次弹出,esp就会加加一次,如图:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接着,把ebp赋给esp,再pop把ebp弹出,此时esp也要移动,此时esp和ebp又回到了先前维护main函数栈帧的样子。如图所示:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此时esp指向的就是call指令的下一条指令的地址,再按一次F10,此时反汇编就会这样:
0003144Bcall00C210E1 00031440addesp,8 00031443movdword ptr [ebp-20h],eax printf("%d", c); 00241456movesi,esp 00241458moveax,dword ptr [ebp-20h] 0024145Bpusheax 0024145Cpush245858h 00241461calldword ptr ds:[00249114h] 00241467addesp,8 0024146Acmpesi,esp 0024146Ccall0024113B return 0; 00241471xoreax,eax }

此时我们就会明白先前存放call指令的下一条指令的地址就是为了方便回来,先前ret执行后esp的位置发生变化:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

此时Add函数的栈帧算是真正销毁,接下来进行main函数栈帧的销毁 。
7、main函数栈帧的销毁
0003144Bcall00C210E1 00031440addesp,8 00031443movdword ptr [ebp-20h],eax printf("%d", c); 00241456movesi,esp 00241458moveax,dword ptr [ebp-20h] 0024145Bpusheax 0024145Cpush245858h 00241461calldword ptr ds:[00249114h] 00241467addesp,8 0024146Acmpesi,esp 0024146Ccall0024113B return 0; 00241471xoreax,eax }

通过反汇编代码得知,此时指向add操作把esp加上8,此时就把x和y这两个形参释放回来了,指向如图所示位置:
C语言|【C终章】函数栈帧的创建和销毁
文章图片

接下来mov把eax放到ebp-20h上,而eax就是我们出Add函数时计算的和,此时和就被我们带回来了,接下来就是main函数栈帧的销毁了,跟上文Add函数栈帧的销毁没有太大区别,这里不多做赘述。
而反汇编代码如下:
00241471xoreax,eax } 00031451popedi 00031452popesi 00031453popebx 00031454addesp,0E4h 0003145Acmpebp,esp 0003145Ccall__RTC_CheckEsp (03113Bh) 00031461movesp,ebp 00031463popebp 00031464ret

四、总结
至此,函数栈帧的创建和销毁正式结束,而本文一开始的几个问题(目标)也能清晰得知:
如下:
  • 1、局部变量是怎么创建的?
首先,为函数分配好栈帧空间并初始化后,然后给局部变量在栈帧里头分配一点空间。
  • 2、为什么局部变量的值是随机值?
因为随机值是我们在开辟栈帧时就放进去的,而我们初始化的时候,就是把随机值给覆盖了。
  • 3、函数是怎么传参的?传参的顺序是怎样的?
当我要调用函数之前,就已经push、push把这两个参数从右向左压栈压进去,当我们真正进入形参函数的时候,在Add函数栈帧里头通过指针的偏移量找到了形参。
  • 4、形参和实参是什么关系?
形参确实是在压栈时开辟的空间,形参和实参只是值上是相同的,空间上是独立的,形参是实参的一份临时拷贝,改变形参不会影响实参。
  • 5、函数调用结束后是怎么返回的?
我们在调用之前就已经把call指令下一条指令的地址给压进去,当函数调用完要返回的时候,就会跳转到call指令下一条指令的地址,返回值是通过寄存器带回来的。

    推荐阅读