前言
我们在C语言的学习过程中可能存在很多困惑
比如
- 局部变量是怎么创建的?
- 为什么局部变量不初始化时的值是随机值?
- 函数是怎么传参的?传参顺序是什么样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后是怎么返回的?
当我们学了今天的内容后,就会对这些内容有更深的理解
1.什么是函数栈帧
栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
2.汇编基础——寄存器和常用汇编指令
2.1寄存器是什么?
CPU由运算器、 控制器、 寄存器等器件构成,这些器件靠内部总线相连。内部总线实现CPU内部各个器件之间的联系, 外部总线实现CPU和主板上其他器件的联系。 简单地说, 在CPU中:
- 运算器进行信息处理;
- 寄存器进行信息存储;
- 控制器控制各种器件进行工作;
- 内部总线连接各种器件, 在它们之间进行数据的传送。
- CPU中的主要部件是寄存器。 寄存器是CPU中程序员可以用指令读写的部件。 程序员通过改变各种寄存器中的内容来实现对CPU的控制。不同的CPU, 寄存器的个数、 结构是不相同的。
常见的寄存器
- eax——累加和结果寄存器
- ebx——数据指针寄存器
- ecx——循环计数器
- edx——i/o指针
- esi——源地址寄存器
- edi——目的地址寄存器
- esp——栈顶寄存器
- ebp——栈底寄存器
今天我们主要用到的寄存器就是==esp==和==ebp==,这两个寄存器中存放的地址是用来==维护函数栈帧的==。
2.2相关指令介绍
- mov——数据传送指令(将一个源操作数送到目的操作数中)
- push——将数据压入栈
- pop——从堆栈弹出数据至指定位置
- sub——减法指令
- add——加法指令
- call——实现对一个函数的调用
- jump——修改eip,转入目标函数进行调用
- ret——恢复返回地址,压入eip
3.栈帧的创建和销毁
函数栈帧创建和销毁的过程在不同环境的编译器下的实现也有所不同这里以VS2019为例
:cat:main函数的调用
这里我们以这段简单而详细的代码为例:
#define _CRT_SECURE_NO_WARNINGS 1
#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\n", c);
return 0;
}
1.首先我们按f10进入调试,找到调用堆栈
这里我们可以看到main函数是被一个叫invoke_main()的函数所调用。
2.接下来我们看一下main函数被调用的过程
3.1main函数的栈帧创建过程
我们进入调试,转到汇编语言,一探究竟
int main()
{
00C918B0 push ebp //将ebp的值压入栈顶,esp会指向ebp
00C918B1 mov ebp,esp //esp的值给ebp,ebp成为新的栈底
00C918B3 sub esp,0E4h //将esp的值减去0E4h(16进制数字),esp的值发生变化;
//新的esp和ebp所围成的空间即为main函数开辟的栈帧
00C918B9 push ebx //向栈顶压入ebx
00C918BA push esi //向栈顶压入esi
00C918BB push edi //向栈顶压入edi
//此时esp指向了edi所在的栈顶
00C918BC lea edi,[ebp-24h] //将[ebp-24]这个地址放在edi里面
00C918BF mov ecx,9 //9放在ecx中
00C918C4 mov eax,0CCCCCCCCh
00C918C9 rep stos dword ptr es:[edi] //从edi位置开始向下的ecx空间
//全部初始化为 0CCCCCCCCh
00C918CB mov ecx,0C9C003h
00C918D0 call 00C9131B
int a = 10;
00C918D5 mov dword ptr [ebp-8],0Ah //把0Ah放在[ebp-8]的位置
int b = 20;
00C918DC mov dword ptr [ebp-14h],14h //把14h放在[ebp-14h]的位置
int c = Add(a, b);
3.2Add函数的栈帧创建过程
int c = Add(a, b);
00C918E3 mov eax,dword ptr [ebp-14h] //把[ebp-14]也就是b的值
//放到eax中
00C918E6 push eax //将eax压入栈顶
00C918E7 mov ecx,dword ptr [ebp-8]//把[ebp-8]也就是a的值放到ecx 中
00C918EA push ecx//将ecx压入栈顶
00C918EB call 00C910B4//调用函数,call指令调用函数之前会把call
// 指令的下一条指令压入栈顶,目的是为了调用
//函数结束之后会回到call指令下一条指令处继续执行
00C918F0 add esp,8
:pig:Add函数内部执行过程
00C91770 push ebp// 来自main函数的ebp被压入栈顶
00C91771 mov ebp,esp//esp的值给ebp,ebp成为新的栈底
00C91773 sub esp,0CCh//将esp的值减去0cch(esp和ebp之间的空间
//为Add函数的调用分配函数栈帧)
00C91779 push ebx //向栈顶压入ebx
00C9177A push esi //向栈顶压入esi
00C9177B push edi //向栈顶压入edi
//此时esp指向了edi所在的栈顶
00C9177C lea edi,[ebp-0Ch]//将[ebp-0ch]这个地址放在edi里面
00C9177F mov ecx,3 //3放在ecx中
00C91784 mov eax,0CCCCCCCCh
00C91789 rep stos dword ptr es:[edi]//从edi位置开始向下的ecx空间
//全部初始化为 0CCCCCCCCh
00C9178B mov ecx,0C9C003h
00C91790 call 00C9131B
00C91790 call 00C9131B
int z = 0;
00C91795 mov dword ptr [ebp-8],0//将z放入[ebp-8]这块空间
z = x + y;
00C9179C mov eax,dword ptr [ebp+8]//[ebp+8]的值放入eax中
00C9179F add eax,dword ptr [ebp+0Ch]//将[ebp+0Ch]
//也就是20加到eax中去=30
00C917A2 mov dword ptr [ebp-8],eax//再将eax的值放在
//[ebp-8]也就是z中
return z;
00C917A5 mov eax,dword ptr [ebp-8]//把[ebp-8]的值放在eax中去
3.3Add函数栈帧的销毁
00C917A8 pop edi//把栈顶元素弹出放到edi中
00C917A9 pop esi//把栈顶元素弹出放到esi中
00C917AA pop ebx//把栈顶元素弹出放到ebx中
00C917B8 mov esp,ebp//将ebp赋给esp,回收了Add开辟的空间
00C917BA pop ebp//弹出栈顶的值存放到ebp中,ebp指向了
//mian函数栈帧的栈底
00C917BB ret//从栈顶弹出call指令下一条指令的地址,
//直接跳转到该指令处,继续执行
回到主函数
00C918F0 add esp,8//esp指向esp+8的地址,相当于把
//形参x,y的空间还给了操作系统
00C918F3 mov dword ptr [ebp-20h],eax//把eax的值
//放到[ebp-20h]这块空间
4.总结
1. 局部变量的创建:
首先为函数分配好栈帧空间,然后给局部变量在栈帧中分配一些空间。
2.未初始化时局部变量的值为什么是随机值:
函数栈帧被创建后编译器会在栈帧所在的空间放入随机值。
3.函数是怎么传参的,传参的顺序是什么:
当还未调用函数时,参数就会被从右向左依次压栈,当真正进入形参函数时,在被调函数的函数栈帧中,通过指针的偏移量找到形参。
4.形参和实参的关系:形参是在压栈时开辟的空间,形参和实参在值上是相同的,但它的空间是独立的,所以形参是实参的一份临时拷贝,改变形参并不会影响实参。
5.函数调用的结果是怎么返回的:
在函数调用之前把call指令下一条指令的地址存进了栈中,使得函数调用结束返回时就可以跳转到该地址处,让函数调用可以返回,返回值通过寄存器带回。
到这里函数栈帧的创建和销毁就结束了,如果有大家觉得有帮助的话还望多多支持。