C语言

您所在的位置:网站首页 c语言中creat函数 C语言

C语言

2023-06-28 07:17| 来源: 网络整理| 查看: 265

函数栈帧 前言:一、认识相关寄存器和汇编指令1.寄存器(寄存器是集成在cpu上的)2.汇编指令 二、函数栈帧创建和销毁的过程1.main函数的调用2.函数栈帧的创建3.函数栈帧的销毁

前言:

为了深入学习C语言,也为了方便理解,我学习了函数栈帧。函数栈帧的创建和销毁能够让我更加深刻的了解编程逻辑和语法。我们学习语法和编程逻辑都是基于封装好的知识上得。因此,我们有必要对函数栈帧的创建和销毁进行学习。本篇博客将用来介绍函数栈帧的创建和销毁的过程,希望大家一起学习。如有不足之处,请大家多多指出,谢谢! 注意: 这里我使用的是vs2022和大家展示。不同编译器上展示的结果会有差异,但大体逻辑一样(也能起到参考的作用)。版本越高的编译器越不好观察,不容易观看函数栈帧创建和销毁的过程,封装过程也会复杂一下。

一、认识相关寄存器和汇编指令 1.寄存器(寄存器是集成在cpu上的)

eax:累加寄存器,相对于其他寄存器,在运算方面比较常用 ebx:基地址寄存器,在内存寻址时存放基地址。 ecx:计数寄存器,用于循环操作,如重复的字符存储操作或者数字统计。 edx:作为EAX的溢出寄存器,总是被用来放整数除法产生的余数。 esi:源变址寄存器,主要用于存放存储单元在段内的偏移量。通常在内存操作指令中作为“源地址指针”使用。 edi:目的变址寄存器,主要用于存放存储单元在段内的偏移量。 ebp:栈底指针 esp:栈顶指针 esp和ebp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧得;esp和ebp用来维护函数栈帧时,正在调用什么函数,就会维护那个函数。 rbp,rsp(64位编译,对于32位编译是ebp,esp寄存器)这2个寄存器中存放的是地址,这2个地址是用来维护函数栈帧的。

2.汇编指令

push: 压栈,给栈顶放一个元素。(数据入栈,同时esp栈顶寄存器也要发生改变) pop: 出栈,给栈顶删除一个元素。(数据弹出至指定位置,同时esp栈顶寄存器也要发生改变) mov:数据转移指令。(后面的指针指向前面) sub:减法命令。(前面的值减后面的值) add:加法命令。 call:函数调用,1. 压入返回地址 2. 转入目标函数 jump:通过修改eip,转入目标函数,进行调用。 lea:加载,把后面的有效地址加载到前面。

补充: 栈区的使用是从高地址到低地址 栈区的使用遵循先进后出,后进先出 栈区的放置是从高地址往低地址放置:push 是压栈 删除是从低往高删除:pop 是出栈 如图: 在这里插入图片描述

二、函数栈帧创建和销毁的过程

本次演示以vs2022为例 演示代码:

#include int ADD(int x,int y) { int z = x + y; return z; } int main() { int a = 3,b=6,c=0; c = ADD(a,b); printf("%d\n", c); return 0; }

准备工作: 1)按F10进入函数调用模式: 在这里插入图片描述 2)打开调用堆栈,出现调用堆栈窗口: 在这里插入图片描述 在这里插入图片描述 3)在调用模式下右击鼠标后,单击转到反汇编,进入反汇编界面: 在这里插入图片描述 在这里插入图片描述

1.main函数的调用

main函数也可以被其他函数调用: 1)为了阅读方便,我们把“显示符号名”取消勾选。 在这里插入图片描述

2)按F10,从调用堆栈,我们可以看到main函数被别的函数调用: 在这里插入图片描述

main()函数被invoke_main()函数调用; invoke_main()函数被__scrt_common_main_seh() 函数调用; __scrt_common_main_seh()函数被__scrt_common_main() 函数调用; __scrt_common_main() 函数被mainCRTStartup(void * __formal) 函数调用。 注意: 编译器版本越高,反汇编越不容易观察,编译器版本过高,会优化。

2.函数栈帧的创建

1)汇编代码如下:

int main() { 00CD18B0 push ebp 00CD18B1 mov ebp,esp 00CD18B3 sub esp,0E4h 00CD18B9 push ebx 00CD18BA push esi 00CD18BB push edi 00CD18BC lea edi,[ebp-24h] 00CD18BF mov ecx,9 00CD18C4 mov eax,0CCCCCCCCh 00CD18C9 rep stos dword ptr es:[edi] 00CD18CB mov ecx,0CDC008h 00CD18D0 call 00CD131B int a = 3, b = 6,c = 0; 00CD18D5 mov dword ptr [ebp-8],3 00CD18DC mov dword ptr [ebp-14h],6 00CD18E3 mov dword ptr [ebp-20h],0 c = ADD(a,b); 00CD18EA mov eax,dword ptr [ebp-14h] 00CD18ED push eax 00CD18EE mov ecx,dword ptr [ebp-8] 00CD18F1 push ecx 00CD18F2 call 00CD1217 00CD18F7 add esp,8 00CD18FA mov dword ptr [ebp-20h],eax printf("%d\n", c); 00CD18FD mov eax,dword ptr [ebp-20h] 00CD1900 push eax 00CD1901 push 0CD7B30h 00CD1906 call 00CD10CD 00CD190B add esp,8 return 0; 00CD190E xor eax,eax } 00CD1910 pop edi 00CD1911 pop esi 00CD1912 pop ebx 00CD1913 add esp,0E4h 00CD1919 cmp ebp,esp 00CD191B call 00CD1244 00CD1920 mov esp,ebp 00CD1922 pop ebp 00CD1923 ret

2)给main函数开辟空间

00CD18B0 push ebp /*压栈,栈顶放一个元素,把ebp寄存器中的值进行压栈,此时的ebp中存放的是 invoke_main函数栈帧的ebp,esp-4*/ 00CD18B1 mov ebp,esp /*把esp的值存放到ebp中,相当于产生了main函数的 ebp,这个值就是invoke_main函数栈帧的esp*/ 00CD18B3 sub esp,0E4h /*sub会让esp中的地址减去一个16进制数字0xe4,产生新的 esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一 个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数 中的局部变量,临时数据已经调试信息等。*/ 00CD18B9 push ebx //将寄存器ebx的值压栈,esp-4 00CD18BA push esi //将寄存器esi的值压栈,esp-4 00CD18BB push edi //将寄存器edi的值压栈,esp-4 /*上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄 存器原来的值,以便在退出函数时恢复。*/ //下面的代码是在初始化main函数的栈帧空间。 //1. 先把ebp-24h的地址,放在edi中 //2. 把9放在ecx中 //3. 把0xCCCCCCCC放在eax中 //4. 将从ebp-0x24h到ebp这一段的内存的每个字节都初始化为CCCCCCCCh 00CD18BC lea edi,[ebp-24h] //把后面有效的地址加载到前面空间里 00CD18BF mov ecx,9 00CD18C4 mov eax,0CCCCCCCCh /*每一次四个字节,总共出了*/ 00CD18C9 rep stos dword ptr es:[edi] //word是一个字两个字节;dword是两个字,四个字节。 00CD18CB mov ecx,0CDC008h //把0CDC008h放在ecx里 00CD18D0 call 00CD131B //执行 call指令之前先会把call 指令的下一条指令的地址进行压栈操作

在这里插入图片描述 图示: 在这里插入图片描述 3)核心代码

int a = 3, b = 6,c = 0;//变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化 00CD18D5 mov dword ptr [ebp-8],3 00CD18DC mov dword ptr [ebp-14h],6 00CD18E3 mov dword ptr [ebp-20h],0 c = ADD(a,b); 00CD18EA mov eax,dword ptr [ebp-14h] 00CD18ED push eax 00CD18EE mov ecx,dword ptr [ebp-8] 00CD18F1 push ecx 00CD18F2 call 00CD1217 00CD18F7 add esp,8 00CD18FA mov dword ptr [ebp-20h],eax

1).给变量a、b、c创建初始化

int a = 3, b = 6,c = 0;//变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化 00CD18D5 mov dword ptr [ebp-8],3 //把3放到ebp-8地址里 00CD18DC mov dword ptr [ebp-14h],6 //把6放到ebp-14h里 00CD18E3 mov dword ptr [ebp-20h],0 //把0放到ebp-20h里

在这里插入图片描述 图示: 在这里插入图片描述 2).调用Add函数

c = ADD(a,b); 00CD18EA mov eax,dword ptr [ebp-14h] //把ebp-14h里的值给eax 00CD18ED push eax //压栈,压一个元素,寄存器eax里压入ebp-14h里面的值 00CD18EE mov ecx,dword ptr [ebp-8] //把ebp-8里的值给ecx 00CD18F1 push ecx //压栈,压一个元素,寄存器exc里压入ebp-8里面的值 00CD18F2 call 00CD1217 /*这条指令是去调用ADD函数,把地址00CD18F7存放到地址00CD18F2里(call指令的下一条指令的地址),按一下F11,进入被调函数ADD里(地址00CD1217),调用结束后,来到了下一条指令的地址处*/ 00CD18F7 add esp,8 00CD18FA mov dword ptr [ebp-20h],eax

图示: 在这里插入图片描述 3).进入ADD函数(在call指令处按F11,然后再按一次F11) 这里我重新进入调试模式,所以地址的位置也就发生了变化,意思还是不变的。

int main() { 00C518B0 push ebp 00C518B1 mov ebp,esp 00C518B3 sub esp,0E4h 00C518B9 push ebx 00C518BA push esi 00C518BB push edi 00C518BC lea edi,[ebp-24h] 00C518BF mov ecx,9 00C518C4 mov eax,0CCCCCCCCh 00C518C9 rep stos dword ptr es:[edi] 00C518CB mov ecx,0C5C008h 00C518D0 call 00C5131B int a = 3, b = 6,c = 0; 00C518D5 mov dword ptr [ebp-8],3 00C518DC mov dword ptr [ebp-14h],6 00C518E3 mov dword ptr [ebp-20h],0 c = ADD(a,b); 00C518EA mov eax,dword ptr [ebp-14h] 00C518ED push eax 00C518EE mov ecx,dword ptr [ebp-8] 00C518F1 push ecx 00C518F2 call 00C51217 00C518F7 add esp,8 00C518FA mov dword ptr [ebp-20h],eax

在这里插入图片描述 在这里插入图片描述 在按一下F11,进入ADD函数里 在这里插入图片描述 4).创建ADD函数栈帧 在这里插入图片描述 5).ADD函数的执行过程

int z = x + y; 00C51795 mov eax,dword ptr [ebp+8] //把ebp+8里面的值给eax 00C51798 add eax,dword ptr [ebp+0Ch] //eax里面的值加上ebp+0Ch地址里的值 00C5179B mov dword ptr [ebp-8],eax //eax的值放到ebp-8地址里 return z; 00C5179E mov eax,dword ptr [ebp-8] //eax相当于全局的寄存器,ebp-8的值放到寄存器里。

如图: 在这里插入图片描述 6),函数栈帧创建的视图: 在这里插入图片描述

3.函数栈帧的销毁

1)ADD函数栈帧的销毁

00C517A1 pop edi //在栈顶弹出一个值,存放到edi中,esp+4 00C517A2 pop esi //在栈顶弹出一个值,存放到esi中,esp+4 00C517A3 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4 00C517A4 add esp,0CCh /*将esp的地址加上0cch,相当于回收了ADD函数的栈帧空间*/ 00C517AA cmp ebp,esp //判断有没有溢出 00C517AC call 00C51244 //call指令里放的是下一个指令的地址 00C517B1 mov esp,ebp //ebp里面的值放到esp里 00C517B3 pop ebp //出栈,弹出一个元素,dsp+4 00C517B4 ret /*call指令可以实现调用一个子程序,在子程序里使用ret指令,结束子程序的执行并返回主函数,让主函数继续往下执行*/

图示: 在这里插入图片描述 2).ADD函数栈帧销毁后,回到主函数: 在这里插入图片描述 调用完ADD函数,回到main函数的时候,继续往下执行,可以看到:

00C518F7 add esp,8 //esp直接+8,相当于跳过了main函数中压栈的 00C518FA mov dword ptr [ebp-20h],eax /*将eax中值,存档到ebp-20h的地址处,其实就是存储到main函数中c变量中,而此时eax中就是ADD函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。*/ printf("%d\n", c);

在这里插入图片描述

注意: 在这里插入图片描述 总结: 1为什么局部变量不初始化内容是随机的或者是"烫"? 因为在创建函数栈帧的时候,中间的地址的值都是不确定的,而如果访问一个未初始化的变量,指向这些不确定的值,就是随机值。而初始化为0CCCCCCCCh时,遇到0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。 2.函数调用时参数时如何传递的?传参的顺序是怎样的? 从创建局部变量的函数(比如main函数)栈帧中通过内存访问,储存在eax和ecx中再入栈(相当于临时拷贝)。 3.函数的形参和实参分别是怎样实例化的? 实参是在函数栈帧里通过ebp内存访问储存的值。形参是由ebp内存访问将栈中储存的临时变量。 4.函数调用结束后怎么返回值? ADD函数中通过将在寄存器(eax)中相加得到的9,在移入ADD函数栈帧中c的地址位置,再将这个地址位置的值传给eax,在销毁ADD函数栈帧后,将eax中的值传给main函数栈帧中创建的c地址位置。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3