函数栈帧(值得收藏)

link

函数栈帧的创建和销毁

前期学习的时候,我们可能有很多困惑?

比如:

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机的?
  • 函数是怎么传参的?传参的顺序是怎么样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

今天知道函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识。

今天我们使用的环境是VS2013,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。

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

寄存器

首先我们来了解一下寄存器。

 这个金字塔是电脑内存的组成部分,分别是硬盘,内存,cache(缓存),寄存器

这幅图从下到上读取速度递增,但存储空间递减,且费用更高。

想必之前应该都有所了解,这里我们只介绍寄存器。

寄存器是CPU的内部组成单元,是CPU运算时取指令和数据的地方,速度很快,寄存器可以用来暂存指令、数据和地址。在CPU中,通常有通用寄存器,如指令寄存器IR;特殊功能寄存器,如程序计数器PC、sp等。

我们常见的寄存器有eax ,ebx, ecx, edx,ebp,esp。

其中后面两个寄存器是用来存放地址的,这两个地址就是用来维护函数栈帧的。

函数栈帧的创建和销毁

前面我们了解到,每一个函数的调用,都要在栈区创建一个空间。

所以我们自然要定义函数,为了方便观察,我们尽量将代码分的足够细。


  
  1. #include<stdio.h>
  2. int Add(int x, int y)
  3. {
  4. int z = 0;
  5. z = x + y;
  6. return z;
  7. }
  8. int main()
  9. {
  10. int a = 10;
  11. int b = 20;
  12. int c = 0;
  13. c = Add(a, b);
  14. printf( "%d\n", c);
  15. return 0;
  16. }

 如图,函数是在栈上创建的,如果这是main函数的函数栈帧,那么当程序运行到main函数时,寄存器ebp和esp中存放的地址就是main函数的首尾地址。

同理,当程序运行到哪个函数时ebp和esp就指向那个函数的空间,而这块空间就叫做这个函数的函数栈帧

根据栈区空间的使用习惯,先使用高地址再使用低地址,所以通常,ebp称为栈底指针,esp称为栈顶指针

然后我们调试起来,观察函数的调用堆栈。

 我们发现,main函数被调用了,那么是被谁调用了呢?

我们继续走,当代码走完会发现是__tmainCRTstartup这个函数内部调用了main函数

然后再将main函数的返回值给mainret。

 而同时,__tmainCRTstartup也是被mainCRTstartup这个函数调用的。

 所以就可以大概了解这个程序的函数在栈中的空间分配。

 那么下面我们就来具体分析到底是怎么做的。

我们按一下F10,然后在调试窗口中找到 转到反汇编 选项

 

 然后就会出现这个界面,这是C语言中对应的汇编代码。

 

 为了能够方便我们观察,我们将这个显示符号名选项关闭。

首先第一步是push

因为执行main函数时__tmainCRTStartup会先调用main函数,所以寄存器ebp和esp首先指向的应该是__tmainCRTStartup函数的空间,如下图所示。

 而push的意思是压栈,即这里是将ebp放到栈顶,而随着栈顶的移动,指向栈顶的esp也应该移动到栈顶,在32位下地址大小为4字节,所以这里也是向上移动(减)4字节。

 mov

 这里就是把esp的值给ebp。

 sub

根据代码,是将esp的值减去0E4h(十六进制),即将esp向上移动0E4h。

而这里ebp和esp所包含的空间发生了变化,其实这里的空间就是为main函数开辟的空间。

 push

 这里push了三次,将ebx,esi,edi 放进栈内。

 lea(load effictive address加载有效地址),mov,mov,rep stos

 根据代码实则是将ebp的地址减去0E4h的地方即edi,向下39h次(直到ebp),将dword(一个word为2个字节,dword为4字节)个字节全部置为0cccccccch。

 

下面才是正式开始执行C语言的有效代码

 

 根据代码,是将0Ah放到ebp-8的位置,并且大小为4个字节,实际意思是为a开辟空间。

而如果创建变量时,没有初始化,变量中就放默认值,即为这里的cccccccc,所以打印时就会出现烫烫烫烫烫烫的情况。 

 然后就是创建b变量和c变量

同理是将14h放到ebp-14h的位置;将0放到ebp-20h的位置。

然后才是调用Add函数

 

首先根据代码是将ebp-14h(即b)的值放到eax里面。

然后将eax压栈。

 同理,将ebp-8(即a)的值放到ecx里面。

然后将ecx压栈。

 其实这两个步骤就是传参。

 

 Add函数中的步骤实则和main函数的创建类似,这里就不再重复。

 

这里是将ebp+8(a)的值放到eax里面,然后将ebp+0Ch(b)的值加到eax中,最后将eax的值放到ebp-8(z)中。

返回时将ebp-8(z)放到eax中,因为函数结束后z会被销毁,而寄存器不会被销毁。

 

 这里的pop是将寄存器出栈,然后将ebp赋给esp

接着继续出栈,把栈顶弹出放到ebp里边。而此时的栈顶就是之前存放的main函数的ebp

pop之后ebp指向main函数的栈底

ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中。

形参的销毁

 

即将esp向下移动,使esp和ebp之间的空间减少,从而达到销毁形参。 

 最后将eax的值放到ebp-20h(c)中。

posted @ 2022-08-19 22:42  luoganttcc  阅读(35)  评论(0编辑  收藏  举报