🌈个人主页:是店小二呀
🌈C语言笔记专栏:C语言笔记
🌈C++笔记专栏: C++笔记
🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
文章目录
- 【前文】
- 一、函数栈帧的概念(stack frame)
- 二、函数栈帧的作用
- 三、函数栈帧的创建和销毁解析
- 3.1 铺垫知识:
- 3.2 认识相关寄存器和汇编指令
- 3.3 解析函数栈帧的创建和销毁
- 【正文】(逐步分析)
- 一、创建main函数的函数栈帧
- 二、main函数变量的创建
- 三、传参操作(在函数调用前)
- 四、函数栈帧的销毁
【前文】
一、函数栈帧的概念(stack frame)
写代码的时候,常会把一个独立实现的功能抽象为函数,所以C语言是以函数为基础单位的。
函数栈帧(stack frame):函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
- 函数参数和返回值
- 临时变量(包括函数的非静态的局部变量以及编译器自动产出的其他临时变量)
- 保存上下信息(包括在函数调用前后需要保持不变的寄存器)
二、函数栈帧的作用
主要解决下面问题:
-
局部变量如何创建的?
-
为什么局部变量不初始化内容是随机的?
-
函数调用时参数时如何传递的?
-
传参的顺序是怎么样的?
-
函数的形参和实参是分别怎么实例化的?
-
函数的返回值如何带会的?
三、函数栈帧的创建和销毁解析
3.1 铺垫知识:
栈(stack):是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
这里的栈,不是数据结构中栈,而是动态内存区域中的栈。在计算机系统中,栈是一个具有先进后出(First In Last Out,FIFO)的属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。(压栈操作使得栈增大,而弹出操作使得栈减小)
在经典的操作系统中,栈总是向下增长(由高地址向低地址)。
3.2 认识相关寄存器和汇编指令
相关寄存器:
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址(函数调用可以看到)
- exc: 计数寄存器
相关汇编命令:
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,1. 压入返回地址 2. 转入目标函数
- jump:通过修改eip(指令寄存器),转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
- lea:装入有效地址,操作数就是地址
3.3 解析函数栈帧的创建和销毁
预备知识:
-
每一次函数调用,都需要在栈中开辟空间(函数栈帧)
-
这块空间需要使用两个寄存器维护:esp(栈顶)和ebp(栈底),ebp记录栈底的地址,esp记录栈顶的地址
-
函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异(本次在vs2019)
函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数,那么内存中以invoke_main 函数开辟好了函数栈帧。
准备环境(便于函数栈帧的过程足够清晰)
转到反汇编(调试到main函数开始执行的第一行,右击鼠标转到反汇编 )
【注意】:VS编译器每次调试都会为程序重新分配内存,对于每次反汇编代码是一次调试代码过程中数据,每次调试略有差异。
【正文】(逐步分析)
一、创建main函数的函数栈帧
00E718B0 push ebp //ebp寄存器中的值压栈,存放invoke_main函数栈帧的ebp,esp-4
00E718B1 mov ebp,esp //mov指令会把move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
00E718B3 sub esp,0E4h ///sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。
00E718B9 push ebx //将寄存器ebx的值压栈,esp-4
00E718BA push esi //将寄存器esi的值压栈,esp-4
00E718BB push edi //将寄存器edi的值压栈,esp-4
//上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。
00E718BC lea edi,[ebp-24h]//ebp-24h的地址,放在edi
00E718BF mov ecx,9 //次数
00E718C4 mov eax,0CCCCCCCCh//0CCCCCCCCh数值存放在eax寄存器
00E718C9 rep stos dword ptr es:[edi] // 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCCCCCCCh(0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”)
注:最后四条代码等价下面的伪代码
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}
二、main函数变量的创建
int a = 3;
00BE183B mov dword ptr [ebp-8],3 //将3存储到ebp-8的地址处,ebp-8的位置其实就是a变量
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5 //将5存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0 //将0存储到ebp-20h的地址处,ebp-20h的位置其实是ret变量
【说明】:
以上汇编代码表示的变量a、b、ret的创建和初始化,这就是局部的变量的创建和初始化。其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的
三、传参操作(在函数调用前)
//调用Add函数
ret = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中
00BE1850 mov eax,dword ptr [ebp-14h] //传递b,将ebp-14h处放的5放在eax寄存器
中
00BE1853 push eax //将eax的值压栈,esp-4
00BE1854 mov ecx,dword ptr [ebp-8] //传递a,将ebp-8处放的3放在ecx寄存器中
00BE1857 push ecx //将ecx的值压栈,esp-4
00BE1858 call 00BE10B4//(函数的目标地址)
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。
-
将main函数的 ebp 压栈
-
计算新的 ebp 和 esp
-
将 ebx , esi , edi 寄存器的值保存
-
计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的
参数,这就是形参访问。 -
将求出的和放在 eax 寄存器尊准备带回
int Add(int x, int y)
{
00BE1760 push ebp //将main函数栈帧的ebp保存,esp-4
00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
00BE1769 push ebx //将ebx的值压栈,esp-4
00BE176A push esi //将esi的值压栈,esp-4
00BE176B push edi //将edi的值压栈,esp-4
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
z = x + y;
//接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}
图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数
在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。
四、函数栈帧的销毁
00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00BE1780 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
00BE1781 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
00BE1782 mov esp,ebp //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈
帧空间
00BE1784 pop ebp //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈
底。
00BE1785 ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行
回到了call指令的下一条指令的地方:
00BE185D add esp,8
//esp直接+8,相当于跳过了main函数中压栈的a'和b'
00BE1860 mov dword ptr [ebp-20h],eax
//将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的.
【注意】:其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。
具体可以参考《程序员的自我修养》一书的第10章。
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二C语言笔记,希望对你在学习C语言中有所帮助!