“总有人间一两风,
填我十万八千梦”
作者:小赛毛
文章初次日期:2022/11/21
目录
函数栈帧解决了什么问题?
什么是栈?
什么是寄存器?
函数栈帧的创建和销毁
预热知识准备:
函数的调用堆栈
转到反汇编
函数栈帧的创建
补充说明:烫烫烫~
调用Add函数:
函数栈帧的销毁
大家学了这么久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函数的栈帧空间
各位老铁们,本章知识到此就要说再见啦,我们接下来的知识,下一篇见,小赛毛与你不见不散!
加油啦,小比特~
记得一键三连嗷!三连!!三连!!!