“总有人间一两风,
填我十万八千梦”
🍑作者:小赛毛
目录
大家学了这么久C语言有没有产生一些问题与疑惑哩?
好家伙,你问俺啥问题,那我只能说6哈哈哈哈

比如噻
老铁这会儿可能又要说,你小子,哪来这么多问题?

其实哩,老铁们呢只要知道函数栈帧的创建和销毁,学会了就明白这些问题了,内功呢自然也就修炼了~
我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:函数参数和函数返回值
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函
数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可
以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出
栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据
从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的
要给大家讲明白这一点呢,就要给大家讲一下寄存器
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址
ebp
esp
这两个呢是今天的重点,要理解函数栈帧,就必须理解这两个寄存器。
ebp,esp中存放的是地址,这2个地址是用来维护函数栈帧的。
同时我们也要知道相关汇编命令
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,1. 压入返回地址 2. 转入目标函数
- jump:通过修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
预热知识准备:
在开始之前呢,再给大家补充一下知识:
每一个函数调用,都要在栈区创建一个空间每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间
- push压栈:给栈顶放一个元素
- pop出栈:从栈顶删除一个元素
我们知道每一个函数调用都会在栈区上开辟一块空间,
如果下面是高地址,上面是低地址
这块空间由ebp esp所维护,在正在调用哪个函数,ebp和esp就去维护哪块函数空间的函数栈帧,比如说此时调用Add函数,那esp ebp就去维护Add函数空间的栈帧,通常我们把ebp叫做栈底指针,esp叫做栈顶指针。
函数的调用堆栈
在vs2013中,main函数也是被其他函数调用的
__tmainCRTStartup
mainCRTStartup
接下来我们继续:
首先f10调试到main函数开始执行的第一行,右击鼠标转到反汇编。
- #define _CRT_SECURE_NO_WARNINGS 1
- #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;
- }
- int main()
- {
- 00C418B0 push ebp //把ebp寄存器中的值进行压栈
- 00C418B1 mov ebp,esp //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp
- 00C418B3 sub esp,0E4h
- 00C418B9 push ebx
- 00C418BA push esi
- 00C418BB push edi
- //下面的代码是在初始化main函数的栈帧空间。
- //1. 先把ebp-24h的地址,放在edi中
- //2. 把9放在ecx中
- //3. 把0xCCCCCCCC放在eax中
- //4. 将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc
- 00C418BC lea edi,[ebp-24h]
- 00C418BF mov ecx,9
- 00C418C4 mov eax,0CCCCCCCCh
- 00C418C9 rep stos dword ptr es:[edi]
- 00C418CB mov ecx,0C4C008h
- 00C418D0 call 00C4131B
- int a = 10;
- 00C418D5 mov dword ptr [ebp-8],0Ah
- int b = 20;
- 00C418DC mov dword ptr [ebp-14h],14h
- int c = 0;
- 00C418E3 mov dword ptr [ebp-20h],0
-
- c = Add(a, b);
- 00C418EA mov eax,dword ptr [ebp-14h]
- 00C418ED push eax
- 00C418EE mov ecx,dword ptr [ebp-8]
- 00C418F1 push ecx
- 00C418F2 call 00C410B4
- 00C418F7 add esp,8
- 00C418FA mov dword ptr [ebp-20h],eax
-
- printf("%d\n", c);
- 00C418FD mov eax,dword ptr [ebp-20h]
- 00C41900 push eax
- 00C41901 push 0C47B30h
- 00C41906 call 00C410D2
- 00C4190B add esp,8
- return 0;
- 00C4190E xor eax,eax
- }

我们先来push压栈
操作前:

我们看执行完:

这就是给大家讲的先把ebp压进去
ebp压进去以后再怎么办呢?
接下里是move
move是把esp给ebp


下一步sub,sub是解码嘛

我们注意到esp变了,这意味着什么呢?
esp的地址往上走了~

紧接着下来三次push ,其实是给顶上压进去了三个元素,这三个元素呢,我们不需要去管

接下来lea
lea:load effective address
给edi里面放了一个地址

将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc
补充说明:烫烫烫~
之所以程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一
个字节都被初始化为cccccccc,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,cccccccc的汉字编码就是“烫”,所以cccccccc被当作文本就是“烫”。
a,b,c创建:
- int a = 10;
- 00C418D5 mov dword ptr [ebp-8],0Ah //将10存储到ebp-8的地址处,ebp-8的位置其实就是a变量
- int b = 20;
- 00C418DC mov dword ptr [ebp-14h],14h //将20存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
- int c = 0;
- 00C418E3 mov dword ptr [ebp-20h],0 //将0存储到ebp-20h的地址处,ebp-20h的位置其实是c变量


以上汇编代码表示的变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化 ,其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的
当a,b,c创建好后,我们就该:
- c = Add(a, b);
- 00C418EA mov eax,dword ptr [ebp-14h] //把b的值20放入eax
- 00C418ED push eax //栈顶压入
- 00C418EE mov ecx,dword ptr [ebp-8] //把a的值10放入ecx
- 00C418F1 push ecx //栈顶压入
- 00C418F2 call 00C410B4 //调用
- 00C418F7 add esp,8
- 00C418FA mov dword ptr [ebp-20h],eax
-
- printf("%d\n", c);
- 00C418FD mov eax,dword ptr [ebp-20h]
- 00C41900 push eax
- 00C41901 push 0C47B30h
- 00C41906 call 00C410D2
- 00C4190B add esp,8
- return 0;
- 00C4190E xor eax,eax
- }
- 00C41910 pop edi
- 00C41911 pop esi
- 00C41912 pop ebx
- 00C41913 add esp,0E4h
- 00C41919 cmp ebp,esp
- 00C4191B call 00C41244
- 00C41920 mov esp,ebp
- 00C41922 pop ebp
- 00C41923 ret
- 00C418EA mov eax,dword ptr [ebp-14h] //把b的值20放入eax
- 00C418ED push eax //栈顶压入
- 00C418EE mov ecx,dword ptr [ebp-8]
- 00C418F1 push ecx
以上两个动作就是在进行传参

- int Add(int x, int y)
- {
- 00C41770 push ebp //将main函数栈帧的ebp保存,esp-4
- 00C41771 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
- 00C41773 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
- 00C41779 push ebx //将ebx的值压栈,esp-4
- 00C4177A push esi //将esi的值压栈,esp-4
- 00C4177B push edi //将edi的值压栈,esp-4
- 00C4177C lea edi,[ebp-0Ch]
- 00C4177F mov ecx,3
- 00C41784 mov eax,0CCCCCCCCh
- 00C41789 rep stos dword ptr es:[edi]
- 00C4178B mov ecx,0C4C008h
- 00C41790 call 00C4131B
- int z = 0;
- 00C41795 mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
- z = x + y;//接下来计算的是x+y,结果保存到z中
- 00C4179C mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
- 00C4179F add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
- 00C417A2 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
- return z;
- 00C417A5 mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
- }
- 00C417A8 pop edi
- 00C417A9 pop esi
- 00C417AA pop ebx
- 00C417AB add esp,0CCh
- 00C417B1 cmp ebp,esp
- 00C417B3 call 00C41244
- 00C417B8 mov esp,ebp
- 00C417BA pop ebp
- 00C417BB ret

图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。
函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢?我们看一下反汇编代码:
- 00C417A8 pop edi //在栈顶弹出一个值,存放到edi中,esp+4
- 00C417A9 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
- 00C417AA pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
- 00C417AB add esp,0CCh //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈帧空间

各位老铁们,本章知识到此就要说再见啦,我们接下来的知识,下一篇见,小赛毛与你不见不散!

加油啦,小比特~
记得一键三连嗷!三连!!三连!!!
