文章目录
前言
前期学习的时候,我们可能有很多困感?
比如:
●局部变量是怎么创建的?I为什么局部变量的值是随机值?
●函数是怎么传参的?传参的顺序是怎样的?
●形参和实参是什么关系?
●函数调用过程是怎样的?
●函数调用结束后是怎么返回的?
学习函数栈帧的创建和销毁,其实就是修炼了自己的内功,也能搞懂后期更多的知识。我使用的环境是vs2019,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。
1.浅谈C语言内存
1.1 内存分配
在C语言中内存分别分为栈区(stack)、堆区(heap)、未初始化全局数据区、已初始化全局数据区、静态常量区(static)、代码区(data)。
1.2 栈
栈区(stack):存放函数参数和局部变量,以及函数调用开辟的栈帧;函数结束返回时自动释放空间。先进后出,向地址减小方向增长(由高地址到低地址),每一个函数调用都要在栈区开辟一块空间。
1.3 寄存器
寄存器往往直接被入在CPU逻辑电路中,速度量快,便于CPU快速调用。例如,我们今天会用到的eax,eba,ecx,edx以及比较重要的esp(栈顶指针),ebp(栈底指针)。这两个寄存器中存放的是地址,是用来维护函数栈帧的。
2. 为main()函数开辟栈帧
我们以以下这段代码为例:
int ADD(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 20;
int b = 10;
int c = 0;
c = ADD(a, b);
printf("%d", c);
return 0;
}
我们将上述代码的反汇编码调用出来,方便我们理解
int main()
{
007518B0 push ebp**//压入一个ebp(栈底)指针**
007518B1 mov ebp, esp**//使得ebp,esp指向同一位置**
007518B3 sub esp, 0E4h**//esp减0E4h(十六进制),也就是为main开辟了一块(esp-0E4h)这么大的空间
如图,为main函数栈帧的开辟过程。
007518B9 push ebx//压入一个ebx
007518BA push esi//压入一个esi
007518BB push edi//压入一个edi
007518BC lea edi, [ebp - 24h]//将[ebp - 24h]这个地址放入edi中
007518BF mov ecx, 9
007518C4 mov eax, 0CCCCCCCCh
007518C9 rep stos dword ptr es : [edi]//从edi往下,每次初始化四个字节,初始化ecx次这么多数据,全部初始化为 CCCCCCCC
/也就是说为main函数开辟的空间全部初始化为CCCCCCCC
如图,为main函数函数栈帧的初始化过程。
3.变量的初始化及函数调用
int a=10;
007518D5 mov dword ptr[ebp - 8], 0Ah//将0Ah(也就是a的值)放在[ebp - 8]这个位置
int b = 20;
007518DC mov dword ptr[ebp - 14h], 14h//将14h(也就是b的值)放在[ebp - 14h]这个位置
int c = 0;
007518E3 mov dword ptr[ebp - 20h], 0//将0(也就是c的值)放在[ebp - 20h]这个位置
c = ADD(a, b);
007518EA mov eax, dword ptr[ebp - 14h]//把b即([ebp - 14h])放到eax中
007518ED push eax//在栈顶压入一个eax
007518EE mov ecx, dword ptr[ebp - 8]//将a放在ecx中
007518F1 push ecx//在栈顶压入一个ecx
007518F2 call 00751217//先将call指令的下一条语句的地址压入,然后调用add函数
007518F7 add esp, 8
007518FA mov dword ptr[ebp - 20h], eax
相信不少小伙伴会疑惑为什么在执行call指令时,要先在栈顶压入call指令下一条语句的地址:,我们知道,我们的代码在编译器中是一条接着一条像下处理编译的,当我们调用结束ADD函数后,要继续执行ADD函数的下一条指令,这是我们提前保存到这个地址就起到了作用,方便我们快速的找到下一条指令,通俗讲就是从哪出去,从哪回来。
4.ADD函数
4.1 ADD函数的创建
int ADD(int x, int y)
{
00F81770 push ebp//压入一个ebp
00F81771 mov ebp, esp//使得压入的ebp指向和esp一样的位置
00F81773 sub esp, 0CCh//esp减0CC(十六进制)开辟了一块空间
00F81779 push ebx//压入一个ebx
00F8177A push esi//压入一个esi
00F8177B push edi//压入一个edi
00F8177C lea edi, [ebp - 0Ch]//将[ebp - 0Ch]这个地址放入edi中
00F8177F mov ecx, 3
00F81784 mov eax, 0CCCCCCCCh
00F81789 rep stos dword ptr es : [edi]//将edi往下,每次初始化四个字节,初始化ecx次这么多数据,全部初始化为 0CCCCCCCCh
4.2 ADD函数使用与销毁
int z = 0;
00F81795 mov dword ptr[ebp - 8], 0//将0放入[ebp - 8]中
z = x + y;
00F8179C mov eax, dword ptr[ebp + 8]//将[ebp + 8](即a)放在eax中
00F8179F add eax, dword ptr[ebp + 0Ch]//将[ebp + 8](即a)与[ebp + 0Ch](即b)加起来
00F817A2 mov dword ptr[ebp - 8], eax//将eax放在[ebp - 8]的位置(即z的位置)
return z;
00F817A5 mov eax, dword ptr[ebp - 8]//将[ebp - 8](即z)放在eax中,由于z是定义在ADD函数中的,因此出ADD函数z就会销毁,所以要将结果放在寄存器eax中
}
008917A8 pop edi//将edi弹出
008917A9 pop esi//将esi弹出
008917AA pop ebx//将ebx弹出
008917B8 mov esp, ebp//使ebp指向esp的位置,即为将ADD开辟的函数栈帧销毁
008917BA pop ebp//将ebp弹出,使得ebp返回到main()函数ebp的位置
008917BB ret//从栈顶弹出我们在main()函数中压入的call指令的下一指令的地址,使得我们找到call指令的下一条指令的位置
如图,为ebp指向esp的位置,即为将ADD开辟的栈帧销毁的示意图。
如图,左下的ebp就是上述的" 008917BA pop ebp//将ebp弹出,使得ebp返回到main()函数ebp的位置 ",的示意图 。
printf("%d", c);
008918FD mov eax, dword ptr[ebp - 20h]//将eax(即z)放在[ebp - 20h](即c)中
00891900 push eax//将eax弹出
00891901 push
897B30h/00891906 call 008910CD
0089190B add esp, 8
return 0;
0089190E xor eax, eax
}
main()函数的销毁与ADD函数的销毁过程一样,我在这里就不在讲解。
5.总结
通过本文的讲解,我们最终了解了为什么创建的局部变量是随机值;函数是怎样传参的:在传值传参中,形参是实参的一份临时拷贝,传参的顺序是从右往左的,等等这样我们前面提到的问题。
最后,创作不易,希望大家能够点赞,关注,,如有错误,或不清楚的地方希望各位读者能够指出来,我会一 一回复。