当前位置: 首页 > news >正文

C++函数栈帧详解

函数栈帧的创建和销毁

在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体取决于编译器的实现!

且需要注意的是,越高级的编译器越不容易观察到函数栈帧的内部的实现;

关于函数栈帧的维护这里我们要重点介绍两个寄存器:ebp和esp

这两个寄存器存放的是用来维护函数栈帧的地址!

每一个函数调用,都要在栈区创建一个空间!

问题:什么是函数栈帧?

函数栈帧实际上就是函数运行时栈上的一块空间!用于存储相对应的临时数据!

接下来讲解以x86系统为例:

这里对函数栈帧管理就是靠这两个寄存器:

  • ebp:被称为基址指针,指向栈帧的底部,(高地址处,且是固定位置!);
  • esp:被称为栈顶指针,指向栈帧的顶部,(低地址处,地址可变);

这里需要注意的是,对于栈来说,是优先使用高地址的(栈的特性!)! 

问题:那么函数的栈帧中都存放了哪些数据?

从低地址到高地址依次存放了:

  • 调用栈帧的基址指针(ebp)--- 用于函数调用后恢复栈帧状态;
  • 下一条指令的地址 --- 执行完该函数后跳转到下一条指令;
  • 被调用的函数的参数(形参) --- 需要注意的是会从右到左进行压栈;
  • 被调用函数的局部变量和临时数据;
  • 寄存器的上下文(例如调用函数期间,使用的ebx、esi、edi等寄存器);

问题:main函数会被其他函数调用吗? 

需要注意的是,main函数也是可以被其他函数调用的:

即__tmainRTStartup会调用main函数!

问题:那么哪个谁调用 __tmainRTStartup这个函数?

__tmainRTStartup会被mainCRTStartup这个函数调用!

所以,这里我们总结一下:

因此,假如说当前我们的main函数里面调用了一个简单的add函数,那么:

中间绿色的框是我们对应的main函数的调用堆栈,而在调用main函数之前,会先调用__tmainRTStartup和mainRTStartup这两个函数!

而add函数在main函数上面,也就是对应的压栈!

问题:但是函数栈帧中具体是怎么进行相关操作的?

示例代码操作

这里我们以一个简单的代码为例,讲解一下对应的相关操作:

其对应的汇编代码如下所示:

需要注意的是,在调用main函数之前,调用main函数的那两个函数的栈帧已经被创建好了!

这里我们对上面出现的汇编指令做一些简单的解释:

  • push实际上就是压栈,将对应的数据压入栈中;
  • mov:实际是就是赋值,这里mov ebp esp实际上就是把esp赋值给ebp;
  • sub:减去对应的地址;
  • lea(load effecitive address):计算内存地址并存入到寄存器当中(不访问内存,仅计算结果);

这里实际上lea到rep stos这四行汇编代码的作用就是将对应的栈的空间的数据都初始化为cc!

  • 压栈:在栈顶上放一个元素;
  • 出栈:从栈顶删除一个元素;

 截止到现在,做的都是初始化相关的任务,此时才开始到函数体内执行对应的任务;

假设每一行代表4个字节:

表示的就是将10这个值放到ebp-8的位置处;

         可以看到,也就是在ebp-8的位置上放10!

        需要注意的是,上面这里我们是把10放进入了,如果没有把10放进入,此时就是默认提供的随机值,因此在C语言中,如果我们没有进行初始化经常会打印出一堆烫烫烫烫(此时就是对应的内存栈上放的是一堆cccc)

此时可以看到对应的内存对其进行了修改(小端存储) 

接下来我们再看int b = 20;这条汇编代码:

dword ptr [ebp-14h], 14h

这段代码实际上就是在ebp-14h这个地址处,填充数字20;

对应的示意图如下所示:

 接下来我们再把int c = 0;也是在对应的栈上进行初始值:

当我们定义好对应的变量时,此时我们会调用add函数:

接下来我们按照对应的汇编代码进行分析:

  • 这里eax指向[ebp -14h],也就是让eax指向b;
  • 然后对eax进行压栈;
  • 接下来让ecx的值指向[ebp-8],也就是ecx指向a;
  • 然后在对ecx进行压栈;

即截止到现在,我们进行的任务就是我们对应的传参工作!

 接下来这里我们要调用对应的call指令:

call指令此时会跳转地址,即这里会跳转到我们对应的红色线框对应的地址!

这里需要注意的是,call完成了两个任务:

  • 将下一条指令的地址(00C21450)进行压栈,压入到栈中;
  • 跳转到对应的地址执行函数体;

接下来就跳转到对应的函数体当中:

其中,上面一堆的逻辑和main函数一样,都是开辟对应的空间,然后进行初始化;

实际上代码逻辑和我们上面讲的是一样的;

此时,我们依然假设每一行是4个字节,即此时每一行可以代表一个整形:

接下来我们依次看对应的汇编代码:

int z = 0;
mov dword ptr [ebp-8,0]

 这里实际上就是把ebp-8指向的这个空间初始化为0;

然后这里把[ebp+8]的值赋值给eax当中:

这里[ebp+8]的值实际上就是之前我们的ecx的值也就是10!

然后再加上[ebp+och]的值,och换算为10进制为12,也就是这里我们之前eax的值!

加完之后,再把算出来的结果返回到ebp-8当中,也就是z!

问题:我们在函数栈帧中有创建对应的形参吗?

没有!在我们call进入函数体之前,我们就通过形参压栈到对应的栈帧当中!

并且参数的压栈顺序是从右向左!

问题:如何再理解形参是实参的一份临时拷贝呢?

这里我们在梳理下逻辑:

  • 在调用add函数之前,会对形参从右到左进行一份拷贝;
  • 而形参是调用函数之前,从main函数里面拷贝的实参!

可以看到上面的ecx和eax是对应的a和b的一份临时拷贝!

所以改变形参不会改变对应的实参!

接下来我们再回到上面对应的代码当中:上面我们只是把计算的值写入到了z当中;

这里我们重点看return z的汇编代码:

mov eax,dword ptr[ebp-8]

这里是把对应的返回值存入到了eax寄存器当中;

需要注意的是寄存器的值不会随着函数栈帧被销毁而丢失!

接下来执行pop对应的汇编代码,这里也就是出栈,对应的esp栈顶指针会进行移动:

由于此时结果已经运行出来,保存到了eax寄存器当中,所以这里接下来直接对栈帧销毁即可!

这里直接进行:

mov ebp,esp

此时栈顶指针直接指向栈底指针!

然后让栈底指针进行出栈:

pop ebp

 实际上就是将add函数的栈底指针出栈,恢复到main函数当中;

pop:不仅对应的空间进行出栈,此时esp还需要+4个字节的地址;

ret

 ret指令实际上就是从栈顶跳出之前call的下一条指令的地址,然后跳转过去;

需要注意的是:ebp和esp维护的是当前执行的函数的栈帧空间,而不是整个程序的栈帧空间!

接下来就返回到执行call之后的部分:

这里执行对esp执行add,实际上就是将对应的压栈的形参进行销毁;

此时上面压栈的两个形参也会被销毁掉; 

然后将eax的值赋值给[ebp - 20h]这个位置!

那么这个ebp-20是什么呢?

实际上就是我们对应的参数c的值,这里把计算返回的值交到c当中!

讲到这里,我们就实现了从计算值然后从函数栈帧返回出来的处理;

main函数的函数栈帧和add的大同小异,所以这里我们就不再过多介绍了!

所以接下来我们就可以回答一些问题了:

问题:为什么局部变量不初始化的时候是随机值呢?

因为这里的局部变量是我们按要求设置的,例如vsstudio都初始化为cc;

问题:函数是如何进行传参的?

实际上当我们还没有调用函数的时候,此时形参就进行从右到左依次进行压栈处理(临时拷贝一份);

问题:函数调用的结果怎么返回?

值保存到寄存器当中,例如eax当中;且call会对下一条指令的地址进行压栈,运行结束后再取出地址;

http://www.xdnf.cn/news/331633.html

相关文章:

  • Ultralytics中的YOLODataset和BaseDataset
  • comfyui 实现中文提示词翻译英文进行图像生成
  • 低成本监控IPC模组概述
  • D盘出现不知名文件
  • int (*)[3]和int (*arr_ptr)[3]区别
  • Spark应用部署模式实例
  • 个人网站versionI正式上线了!Personal Website for Jing Liu
  • ✍️【TS类型体操进阶】挑战类型极限,成为类型魔法师!♂️✨
  • JAVA八股文
  • CI/CD与DevOps流程流程简述(提供思路)
  • 使用pdm管理python项目时去哪里找nuitka
  • 如何通过复盘提升团队能力?
  • 数组和集合
  • 【C++的类型转换】
  • 【漏洞预警】:致远OA V8.1 SP2 data.htm DOM型XSS漏洞
  • 使用 `detach()` 断开与共享特征层的连接
  • (已完结)完美解决C盘拓展卷是灰色的无法扩容的问题以及如何正确地在WINDOS上从一个盘扩容到C盘
  • Android 如何理解 Java JNI 中的引用与 Java 对象应用的区别
  • java算法的核心思想及考察的解题思路
  • Codeforces Round 1022 (Div. 2)
  • YOLOv1:开创实时目标检测新纪元
  • go.mod没有自动缓存问题
  • vue截图-html2canvas
  • 《硬件视界》专栏介绍(持续更新ing)
  • Qt学习Day2:信号槽
  • 从SQL的执行流程彻底详解预编译是如何解决SQL注入问题
  • Linux57配置MYSQL YUM源
  • 离散化(竞赛)
  • MinIo安装和使用操作说明(windows)
  • C++相关学习过程