你知道函数栈帧的创建和销毁吗?

文章目录

  • 前言
  • 观图有感
  • 一、概述
  • 二、寄存器
  • 三、汇编指令
  • 四、函数栈帧的创建
    • 4.1 main函数栈帧的创建
      • push ebp
      • mov ebp,esp
      • sub esp,0E4h
      • push ebx / esi /edi
      • lea edi,[ebp-24h] 、mov ecx,9、mov eax,0CCCCCCCCh、rep stos dword ptr es:[edi]
      • main函数中变量的创建
    • 4.2 在main函数中调用Add函数
      • 传参
      • 进入Add函数
      • Add函数中变量Z的创建
      • z=x+y
  • 五、函数栈帧的销毁
  • 总结

前言

在前面的学习中,可能会出现许多疑惑:
1、局部变量是怎么创建的?
2、函数是怎么传参的?
3、函数调用是怎么做到的?
4、函数调用结束后是怎么返回的?

希望读者在看完小编的文章,对一系列问题会有所掌握

观图有感

你去野外烧烤,并为此创建了一个待办事项清单——一叠便条。

你知道函数栈帧的创建和销毁吗?_第1张图片

将想到的烧烤食物写在便条上,一个食材一个便条,最先想到的食材写在便条上后,放在最下面,依次往上放,最后想到的写在便条上后,放在最上面。

之后,在烧烤的时候,从上往下拿,拿出来的表示你已经在烧烤了,可以将它删去。

一叠便条要简单得多:插入的待办事项放在清单的最前面;读取待办事项时,你只读取最上面的那个,并将其删除。因此这个待办事项清单只有两种操作:压入(插入)和弹出(删除并读取)。

你知道函数栈帧的创建和销毁吗?_第2张图片

你知道函数栈帧的创建和销毁吗?_第3张图片

这种数据结构称为。栈是一种简单的数据结构,之前学函数的时候我们一直在使用它,却没有意识到!

一、概述

函数栈帧是在内存中的栈区为被调函数开辟的一块空间,里面用来存放该函数中定义的变量等东西,当函数运行完毕栈帧将被销毁。

可以想象成洗盘子,最先吃完的人将盘子放在最下面,后面吃完的人依次将盘子叠放在前一个的上面。于是,最后吃完的人的盘子就在最上面,也就是最先洗。

Push(入栈):为栈增加一个元素
Pop (出栈): 从栈中取出一个元素

二、寄存器

寄存器是中央处理器内用来暂存指令、数据和地址的电脑存储器。寄存器的存贮容量有限,读写速度非常快。在计算机体系结构里,寄存器存储在已知时间点所作计算的中间结果,通过快速地访问数据来加速计算机程序的执行。
–百科

Name Function
eax “累加器”, 用来存放函数的返回值
ebx "基地址"寄存器,可作为储存器指针来使用, 在内存寻址时存放基地址
ecx 计数器, 在循环和指针操作时,要用它来控制循环次数
edx "数据寄存器’,在进行乘、除法运算时,可作为默认的操作数参数参与运算
esp 栈指针寄存器,存放函数栈顶地址
ebp 帧指针寄存器,存放函数栈底地址

esp和ebp这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的
在本节中,主要了解这俩寄存器

三、汇编指令

Directives Function
push x 将x压入栈中
pop x 将x弹出栈中
mov a, b 将b赋值给a,即b指向a
sub a num a的值减去num,即a向低地址移动
lea(load effective adress) 加载有效地址(在示例中理解)

四、函数栈帧的创建

所有函数的调用都会在内存里面的栈区创建函数栈帧,包括main函数。

以下面一个详细的代码,描述函数栈帧的创建
本次代码是在 vs 2013 里面实现的,版本越低,可以更好展示

#include 
int Add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	return 0;
}

按F10,进行调试
你知道函数栈帧的创建和销毁吗?_第4张图片

4.1 main函数栈帧的创建

C语言所对应的汇编代码
你知道函数栈帧的创建和销毁吗?_第5张图片

int main()
{
  push        ebp           //将ebp压入栈中
  mov         ebp,esp       //将esp赋值给ebp,即将esp移动到ebp的位置
  sub         esp,0E4h      //将esp向低地址移动0E4h个字节的位置
  push        ebx           //(我们不要管)
  push        esi           //(我们不要管)
  push        edi           //(我们不要管)
  lea         edi,[ebp-24h]  //将[ebp-24h]存入edi中
  mov         ecx,9          //将9存入ecx中
  mov         eax,0CCCCCCCCh  //将0CCCCCCCCh存入eax中
  rep stos    dword ptr es:[edi]  //将edi的值对应的地址处开始,将高于该地址共ecx个单位的值置为0CCCCCCCCh
  
	int a = 10;
  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
  mov         dword ptr [ebp-14h],14h  
	int c = Add(a, b);
  mov         eax,dword ptr [ebp-14h]  
  push        eax  
  mov         ecx,dword ptr [ebp-8]  
  push        ecx  
  call        011C10B4  
  add         esp,8  
  mov         dword ptr [ebp-20h],eax  
	return 0;
  xor         eax,eax  
}
  pop         edi  
  pop         esi  
  pop         ebx  
  add         esp,0E4h  
  cmp         ebp,esp  
  call        011C1235  
  mov         esp,ebp  
  pop         ebp  
  ret  

看着有点麻烦,不过对着汇编语言,可以仔细研究一番。

首先看main函数

你知道函数栈帧的创建和销毁吗?_第6张图片

栈使用空间是由高地址到低地址
正在调用哪个函数,esp和ebp就维护哪个函数,在这里,我们调用的是main函数,那么就维护main函数。

通过 __tmainCRTStartup 函数调用main函数,所以要创建好__tmainCRTStartup 的栈帧

push ebp

你知道函数栈帧的创建和销毁吗?_第7张图片

push ebp就是把__mainCRTStartup 函数栈底的地址压栈,ebp的值压入后,esp指针会上移一位

mov ebp,esp

你知道函数栈帧的创建和销毁吗?_第8张图片

sub esp,0E4h

你知道函数栈帧的创建和销毁吗?_第9张图片

push ebx / esi /edi

你知道函数栈帧的创建和销毁吗?_第10张图片

lea edi,[ebp-24h] 、mov ecx,9、mov eax,0CCCCCCCCh、rep stos dword ptr es:[edi]

你知道函数栈帧的创建和销毁吗?_第11张图片

main函数中变量的创建

你知道函数栈帧的创建和销毁吗?_第12张图片
你知道函数栈帧的创建和销毁吗?_第13张图片

4.2 在main函数中调用Add函数

你知道函数栈帧的创建和销毁吗?_第14张图片

	int a = 10;
  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
  mov         dword ptr [ebp-14h],14h  
	int c = Add(a, b);
  mov         eax,dword ptr [ebp-14h]  //把ebp-14h这个地址里面存放的值(也就是20)赋值给eax
  push        eax    //push(压栈)eax
  mov         ecx,dword ptr [ebp-8]  //把ebp-8这个地址里面存的值(也就是10)赋值给ecx
  push        ecx      //push(压栈)ecx
  
  
  call        011C10B4  
  add         esp,8  
  mov         dword ptr [ebp-20h],eax  

传参

经过这两次压栈后,esp指针指向的地址减小8个字节(一次减小4个字节,也就是1个整型)

你知道函数栈帧的创建和销毁吗?_第15张图片

进入Add函数

call 011C10B4 执行这条语句,在执行这条语句时,我们需要按键盘上的F11(笔记本电脑可能需要按Fn+F11)。

在按完F11后,我们就进入了Add函数内部,而且还会发现,esp指针还向上移动了4个字节

你知道函数栈帧的创建和销毁吗?_第16张图片
003F1450 是call指令的下一条指令的地址。

为什么要将call指令的下一条指令的地址存起来呢??
是因为在Add函数调用结束的时候,需要返回继续执行call指令的下一条指令,所以在执行call指令的时候要将call下一条指令的地址存起来,在Add函数调用结束的时候,就能根据存的地址找到call指令的下一条指令,从而让程序可以继续执行。

紧接着继续按F11,就真正来到了Add函数里面

剩下的过程其实和在调用main函数的动画演示是一样的,不再做过多演示。

Add函数中变量Z的创建

此过程和main函数中变量a,b,c创建的过程是一样的

你知道函数栈帧的创建和销毁吗?_第17张图片

z=x+y

int z = x + y;
  mov         eax,dword ptr [ebp+8]  //把ebp+8这个地址里面存储的值放到eax里
  add         eax,dword ptr [ebp+0Ch]  //把ebp+0Ch这个地址里面存储的值加到eax里面去
  mov         dword ptr [ebp-8],eax  //把eax里面的值存到ebp-8这个地址里面(变量z的地址)
 return z;
  mov         eax,dword ptr [ebp-8]

五、函数栈帧的销毁

  pop         edi  
  pop         esi  
  pop         ebx  
  add         esp,0CCh  

会发现pop指令,代表出栈
将edi,esi和ebx弹出栈

add esp,8这条指令,该指令的执行结果是让esp指针指向的地址加8
你知道函数栈帧的创建和销毁吗?_第18张图片
mov dword ptr [ebp-20h],eax指令。执行结果是:把eax里面存的值(30)赋值给ebp-20h所指向的这块空间,其实就是变量c的存储空间

执行这条指令之前ebp-20h所指向的这块空间存的值是0,在执行完这条指令之后,ebp-20h所指向的这块空间里面存的就是1e(十进制下的30)。并且可以看出ebp-20h和&c的值相同,说明他们指向同一块空间——变量c的存储空间.

总结

本节涉及一些数据结构的内容,但是为了解决心中的疑惑,我们可以了解一下。
如有误,欢迎指正!!

你可能感兴趣的:(c语言,学习,数据结构)