Hello,大家好!!!这里是小周为您带来的呕心沥血之作------C语言秘籍之函数栈帧的创建和销毁!!跟着小周学定会让你C语言功力大成,称霸武林,话不多说,马上开讲!!!!!
本章主题:
-
什么是函数栈帧?
-
理解函数栈帧能解决什么问题?
-
函数栈帧的创建和销毁解析?
1、什么是函数栈帧
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
-
函数参数和函数返回值
-
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
-
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)
2、理解函数栈帧能解决什么问题呢
理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的理解了:
-
局部变量是如何创建的?
-
为什么局部变量不初始化内容是随机的?
-
函数调用时参数时如何传递的?传参的顺序是怎样的?
-
函数的形参和实参分别是怎样实例化的?
-
函数的返回值是如何带会的?
让我们一起走进函数栈帧的创建和销毁的过程中。
3、函数栈帧的创建和销毁解析
3.1 什么是栈
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的书,先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。
3.2 认识相关寄存器和汇编指令
相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
相关汇编命令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
3.3 解析函数栈帧的创建和销毁
在不同的编译器当中,出现的情况各有不同,每一个函数调用,都要在栈区上创建一块空间
#include<stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 10;
int c = 10;
c = Add(a, b);
printf("%d\n", c);
}
我们调试然后打开调用堆栈
我们点一下F10,发现main函数被调用,那是谁调用的呢?怎样看出来呢?
我们右击选择显示外部代码
这样就可以看清楚main函数被谁调用了
我们发现是 __scrt_common_main_seh() 调用 invoke_main() ,然后invoke_main() 调用main函数
在栈区中是这样的
具体怎么做的,我们来研究
我们重新调试一下
这个时候,右击鼠标,转到反汇编
你可以自己改变反汇编的位置,我将他放在右边好为大家讲解
因为之前我给大家将是invoke_main() 调用的main函数,现在来到了main函数,这时之前的invoke_main() 函数的函数栈帧已经创建好了
int main()
{
00F518B0 push ebp
00F518B1 mov ebp,esp
00F518B3 sub esp,0E4h
00F518B9 push ebx
00F518BA push esi
00F518BB push edi
00F518BC lea edi,[ebp-24h]
00F518BF mov ecx,9
00F518C4 mov eax,0CCCCCCCCh
00F518C9 rep stos dword ptr es:[edi]
00F518CB mov ecx,0F5C003h
00F518D0 call 00F5131B
int a = 10;
00F518D5 mov dword ptr [ebp-8],0Ah
int b = 10;
00F518DC mov dword ptr [ebp-14h],0Ah
int c = 0;
00F518E3 mov dword ptr [ebp-20h],0
c = Add(a, b);
00F518EA mov eax,dword ptr [ebp-14h]
00F518ED push eax
00F518EE mov ecx,dword ptr [ebp-8]
00F518F1 push ecx
00F518F2 call 00F510B4
00F518F7 add esp,8
00F518FA mov dword ptr [ebp-20h],eax
printf("%d\n", c);
00F518FD mov eax,dword ptr [ebp-20h]
00F51900 push eax
00F51901 push 0F57B30h
00F51906 call 00F510D2
00F5190B add esp,8
}
00F5190E xor eax,eax
00F51910 pop edi
00F51911 pop esi
00F51912 pop ebx
00F51913 add esp,0E4h
00F51919 cmp ebp,esp
00F5191B call 00F51244
00F51920 mov esp,ebp
00F51922 pop ebp
00F51923 ret
00F518B0 push ebp
1
我们打开监视窗口通过地址来看一下
按一下F10,预想的情况应该是esp地址降低
确实是这样,那我们再打开内存看一下
同样证明压栈成功,我们再接着来
00F518B1 mov ebp,esp
2
按一下F10,预想的情况应该是ebp的值变成与esp相同的值
00F518B3 sub esp,0E4h
3
按一下F10,确实如此,esp的值对应减小了,0E4h-8进制数字是228
因为现在已经调用main函数里面去了,该为main函数做点什么了,所以这个空间就是main函数的函数栈帧
0x007BFB74 01 18 f5 00 ..?.
0x007BFB78 23 10 f5 00 #.?.
0x007BFB7C 23 10 f5 00 #.?.
0x007BFB80 00 b0 90 00 .??.
0x007BFB84 90 fb 7b 00 ??{.
0x007BFB88 65 fb 80 79 e?€y
0x007BFB8C 9c fb 7b 00 ??{.
0x007BFB90 b3 ee 80 79 ??€y
0x007BFB94 ee b3 03 56 ??.V
0x007BFB98 13 00 00 00 ....
0x007BFB9C b8 fb 7b 00 ??{.
0x007BFBA0 b8 fb 7b 00 ??{.
0x007BFBA4 3c 14 81 79 <.?y
0x007BFBA8 b8 fb 7b 00 ??{.
0x007BFBAC b3 ee 80 79 ??€y
0x007BFBB0 ee b3 03 56 ??.V
0x007BFBB4 13 00 00 00 ....
0x007BFBB8 d4 fb 7b 00 ??{.
0x007BFBBC d4 fb 7b 00 ??{.
0x007BFBC0 3c 14 81 79 <.?y
0x007BFBC4 23 10 f5 00 #.?.
0x007BFBC8 80 68 14 01 €h..
0x007BFBCC c0 ca 7d 76 ??}v
0x007BFBD0 e0 fb 7b 00 ??{.
0x007BFBD4 b3 ee 80 79 ??€y
0x007BFBD8 ee b3 03 56 ??.V
0x007BFBDC 13 00 00 00 ....
0x007BFBE0 fc fb 7b 00 ??{.
0x007BFBE4 fc fb 7b 00 ??{.
0x007BFBE8 3c 14 81 79 <.?y
0x007BFBEC ee b3 03 56 ??.V
0x007BFBF0 13 00 00 00 ....
0x007BFBF4 c0 ca 7d 76 ??}v
0x007BFBF8 10 fc 7b 00 .?{.
0x007BFBFC db ca 7d 76 ??}v
0x007BFC00 02 00 00 00 ....
0x007BFC04 b9 ab 72 77 ??rw
0x007BFC08 00 00 00 00 ....
0x007BFC0C 00 00 00 00 ....
0x007BFC10 24 fc 7b 00 $?{.
0x007BFC14 fc 19 81 79 ?.?y
0x007BFC18 02 00 00 00 ....
0x007BFC1C 7a 33 f7 ee z3??
0x007BFC20 c0 ca 7d 76 ??}v
0x007BFC24 40 fc 7b 00 @?{.
0x007BFC28 33 d0 7d 79 3?}y
0x007BFC2C 00 00 00 00 ....
0x007BFC30 23 10 f5 00 #.?.
0x007BFC34 00 00 00 00 ....
0x007BFC38 5e ef 80 79 ^?€y
0x007BFC3C 74 60 8e 79 t`?y
0x007BFC40 54 fc 7b 00 T?{.
0x007BFC44 42 f2 80 79 B?€y
0x007BFC48 23 10 f5 00 #.?.
0x007BFC4C 01 00 00 00 ....
0x007BFC50 01 00 00 00 ....
0x007BFC54 60 fc 7b 00 `?{.
0x007BFC58 78 fc 7b 00 x?{.
这就是预开辟好的,我们继续往下走
00F518B9 push ebx
00F518BA push esi
00F518BB push edi
4
再按F10
再按F10
lea-load effective address 加载有效地址
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-24h到ebp这一段的内存的每个字节都初始化为0xCC
1个word两个字节,dword就是双字,4个字节
上面的这段代码最后4句,等价于下面的伪代码:
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}
小知识:烫烫烫~
int a = 10;
00F518D5 mov dword ptr [ebp-8],0Ah
我们可以通过调试来看,确实内存中a的值改为了10
int b = 10;
00F518DC mov dword ptr [ebp-14h],0Ah
b的值是10
int c = 0;
00F518E3 mov dword ptr [ebp-20h],0
//调用Add函数
c = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中
00F518EA mov eax,dword ptr [ebp-14h] //传递b,将ebp-14h处放的10放在eax寄存器中
00F518ED push eax //将eax的值压栈,esp-4
00F518EE mov ecx,dword ptr [ebp-8] //传递a,将ebp-8处放的10放在ecx寄存器中
00F518F1 push ecx //将ecx的值压栈,esp-4
//跳转调用函数
00F518F2 call 00F510B4
00F518F7 add esp,8
00F518FA mov dword ptr [ebp-20h],eax
这时按F11
这个00F518F7这是谁呢?call指令把add函数的地址压栈了
再按F11
这些是在为Add函数的栈帧做准备,此时main函数的函数栈帧变大了
此时来到了add函数内部,我们发现很熟悉
00F51770 push ebp
00F51771 mov ebp,esp
00F51773 sub esp,0CCh
00F51779 push ebx
00F5177A push esi
00F5177B push edi
00F5177C lea edi,[ebp-0Ch]
00F5177F mov ecx,3
00F51784 mov eax,0CCCCCCCCh
00F51789 rep stos dword ptr es:[edi]
我不多做介绍,直接看图
int z = 0;
00F51795 mov dword ptr [ebp-8],0
z = x + y;
00F5179C mov eax,dword ptr [ebp+8]
00F5179F add eax,dword ptr [ebp+0Ch]
00F517A2 mov dword ptr [ebp-8],eax
那我们发现这x和y在哪里呀?在下面已经存好了
形参是从右往左传,b先压栈,然后是a压栈,证明了形参不是在Add函数内部创建的,而是回来找了传参传过去的那个空间
return z;
00F517A5 mov eax,dword ptr [ebp-8]
z出函数自动销毁,先把z的值放在eax寄存器中,否则出函数以后,数据会丢失
00F517A8 pop edi
00F517A9 pop esi
00F517AA pop ebx
跟之前对比,我们发现esp的地址也确实增大了,因为弹出栈,栈中元素减少,下方是高地址
00F517B8 mov esp,ebp
00F517BA pop ebp
00F517BB ret
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。
-
将main函数的 ebp 压栈
-
计算新的 ebp 和 esp
-
将 ebx ,esi, edi 寄存器的值保存
-
计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的参数,这就是形参访问。
-
将求出的和放在 eax 寄存器中准备带回
5
大家看好这个ret,在视频里我没演示,因为怕大家乱,再按一下F10我们发现
回到了main函数,接着执行,因为当时进Add函数之前,我们就把00F518F7这个地址存进去了
00F518F7 add esp,8
esp的地址加8跳过形参的两个空间,将形参的空间还给操作系统
6
00F518FA mov dword ptr [ebp-20h],eax
//将eax中的值,存到ebp-0x20的地址处,其实就是存储到main函数中ret变量中
//而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的
//程序是在函数调用返回之后,在eax中去读取返回值的。
返回值是首先放到寄存器里面去,当我们真的回到这个函数里来的时候,再把这个值放到c中,返回值就带回来了
printf("%d\n", c);
00F518FD mov eax,dword ptr [ebp-20h]
00F51900 push eax
00F51901 push 0F57B30h
00F51906 call 00F510D2
00F5190B add esp,8
}
00F5190E xor eax,eax
00F51910 pop edi
00F51911 pop esi
00F51912 pop ebx
00F51913 add esp,0E4h
00F51919 cmp ebp,esp
00F5191B call 00F51244
00F51920 mov esp,ebp
00F51922 pop ebp
00F51923 ret
这些代码和Add函数中的很类似,大家下去可以自己琢磨琢磨调试一下,这里我不再多做讲解,不懂得可以来问小周
拓展了解:
其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。具体可以参考《程序员的自我修养》一书的第10章。
到这里我们给大家完整的演示了main函数栈帧的创建,Add函数栈帧的创建和销毁的过程,相信大家已经能够基本理解函数的调用过程,函数传参的方式,也能够回答本章开始提出的问题了。
问题解答:
1、局部变量怎么创建的?
局部变量的创建是,首先我为这个函数分配好栈帧空间,栈帧空间里面我们初始化好一部分空间,然后给我的局部变量在栈帧里分配一点空间
2、为什么局部变量不初始化内容是随机的?
因为随机值是我们放进去的(0xCCCCCCCC),初始化是把随机值覆盖了
3、函数调用时参数时如何传递的?传参的顺序是怎样的?
当我们调用那个函数的时候,我们已经push将对应的参数从右向左开始压栈压进去,当我们真正进入形参的这个函数时,其实在对应的栈帧空间,通过指针的偏移量找到了形参
4、函数的形参和实参是什么关系?
形参确实是我在压栈时候开辟的空间,他和我的实参只是值相同,空间是独立的,实参是我形参的一份临时拷贝,改变形参不会影响实参
5、函数的返回值是如何带回的?
调用之前就把call指令的下一条指令的地址记住了,压入栈里面了,把调用这个函数的上一个函数的栈帧的ebp存进去了,当我们函数调用完返回的时候,弹出ebp,就能够找到上一个函数调用的ebp,然后指针往下走找到esp的地址,回到上一个函数的栈帧空间,由于我们记住了call指令的下一条指令的地址,所以当我们函数调用返回时,就可以跳转到对应的位置,返回值是放到寄存器里面去,当我们真的回到这个函数里来的时候,再把这个值放到对应需要存放的变量中,返回值就带回来了
好啦!!!学到这你已经很厉害了!!!给自己竖一个大拇指!!!你是最棒的!!!今天小周就带大家学到这里。小周写一篇高质量详细的博客也也花了好多时间,看到这里请动动你的小手来为小周点赞推广评论一下吧!!!你们的点赞推广评论是我继续输出高质量博客的动力!!!谢谢大家!!!
欲知后事如何,且听下回分解