大家可能会函数栈帧不了解,可能都没有听过这个,不用着急,在理解函数栈帧之前,我们先来了解一下程序对内存使用的分区大概情况:
区域 | 作用 |
栈区(stack) | 由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似与数据结构中的栈 |
堆区(heap) | 一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表 |
静态区(static) | 全局变量和静态变量存放于此 |
那么函数栈帧又是什么呢?从逻辑上讲栈帧就是一个函数执行的环境。际上,栈帧可以简单理解为:栈帧就是存储在栈上的每一次函数调用涉及的相关信息的记录单元。
栈是从高地址向低地址分配内存的。每调用一个函数的时候,函数都有它自己的函数栈帧去维护,这个栈帧中维护着所需要的各种信息。
再了解函数栈帧的概念之后,我们还需要学习一些寄存器:
寄存器名称 | 作用 |
eax | 累加(Accumulator)寄存器,常用于函数返回值 |
ebx | 基址(Base)寄存器,以它为基址访问内存 |
ecx | 基址(Base)寄存器,以它为基址访问内存 |
edx | 数据(Data)寄存器,常用于乘除法和I/O指针 |
esi | 源变址寄存器 |
dsi | 目的变址寄存器 |
esp | 堆栈(Stack)指针寄存器,指向堆栈顶部 |
ebp | 基址指针寄存器,指向当前堆栈底部 |
eip | 指令寄存器,指向下一条指令的地址 |
下面用一个简单的函数调用去介绍函数栈帧:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d", c);
return 0;
}
然后进去vs2019的函数调试中,进入反汇编看函数的具体过程,首先是函数的初始化(我的是从高地址指向低地址,是往上画的):
int main()
{
00B018B0 push ebp
00B018B1 mov ebp,esp
00B018B3 sub esp,0E4h
00B018B9 push ebx
00B018BA push esi
00B018BB push edi
00B018BC lea edi,[ebp-24h]
00B018BF mov ecx,9
00B018C4 mov eax,0CCCCCCCCh
00B018C9 rep stos dword ptr es:[edi]
00B018CB mov ecx,0B0C003h
00B018D0 call 00B0131B
前面的布置都是main函数的栈帧
int a = 10;
00B018D5 mov dword ptr [ebp-8],0Ah
int b = 20;
00B018DC mov dword ptr [ebp-14h],14h
int c = add(a, b);
00B018E3 mov eax,dword ptr [ebp-14h]
00B018E6 push eax
00B018E7 mov ecx,dword ptr [ebp-8]
00B018EA push ecx
00B018EB call 00B01023
int add(int x, int y)
{
00B01850 push ebp
00B01851 mov ebp,esp
00B01853 sub esp,0CCh
00B01859 push ebx
00B0185A push esi
00B0185B push edi
00B0185C lea edi,[ebp-0Ch]
00B0185F mov ecx,3
00B01864 mov eax,0CCCCCCCCh
00B01869 rep stos dword ptr es:[edi]
00B0186B mov ecx,0B0C003h
00B01870 call 00B0131B
这里可以看和和main函数初始化的时候基本上没有什么差别,这里就不做讲解,留给读者自己去思考,可参考前面main函数初始化的情况去看。
int z = 0;
00B01875 mov dword ptr [ebp-8],0
z = x + y;
00B0187C mov eax,dword ptr [ebp+8]
00B0187F add eax,dword ptr [ebp+0Ch]
00B01882 mov dword ptr [ebp-8],eax
return z;
00B01885 mov eax,dword ptr [ebp-8]
}
00B01888 pop edi
00B01889 pop esi
00B0188A pop ebx
00B0188B add esp,0CCh
00B01891 cmp ebp,esp
00B01893 call 00B01244
00B01898 mov esp,ebp
00B0189A pop ebp
00B0189B ret
esp+0CCh为什么就指向了ebp的位置,大家可以直接动手去调试,看看内存那一块,就知道为什么了 ,然后cmp指令到pop ebp指令都是在销毁add函数的栈帧
ret指令让我们回到了第一次调用add函数的地方(因为在调用的时候,我们已经记住了这个地址,所以可以返回到main函数的栈帧里面去):
00B018F0 add esp,8
00B018F3 mov dword ptr [ebp-20h],eax
printf("%d", c);
00B018F6 mov eax,dword ptr [ebp-20h]
00B018F9 push eax
00B018FA push 0B07B30h
00B018FF call 00B010D2
00B01904 add esp,8
return 0;
然后就是在ebp-20h的位置创建了局部变量c,并把eax的值赋给了它,后面在进行打印,最后把main函数的栈帧销毁(和add函数的栈帧的销毁类似)
总结一下:1.局部变量的创建是在为当前包含局部变量的函数初始化一片空间之后,在这个空间内给局部变量进行初始化;2.为什么局部变量不初始化的时候是随机值,我们可以看见前面在对函数栈帧进行初始化的时候,里面的被分配的空间值一部分被赋值成了0CCCCCCCCh这个值,不同的编译器初始化的值也可能不同,而局部变量的创建是在那些被初始化为随机值的地方去创建的,所以说局部变量不进行初始化的时候就是随机值;3.函数传参的顺序,我们也可以看到是从右往左传参的,并且形参是实参的临时拷贝,因为在调用函数之前我们已经把实参进行了拷贝并且压入栈中了;
由于本人对函数栈帧的了解还不够深入,只能给各位讲到现在这样,以后还会发布关于函数栈帧的文章,到时候会更加详细的给大家进行讲解,望大家理解,希望这篇文章能给大家带来一点帮助,上述的内容有什么不对的地方,欢迎大家在评论区留言指正。