提升C内功--函数栈帧的创建和销毁(动画讲解)

前言
作者龟龟不断向前
简介宁愿做一只不停跑的慢乌龟,也不想当一只三分钟热度的兔子。
专栏:C++初阶知识点

工具分享

  1. 刷题: 牛客网 leetcode
  2. 笔记软件:有道云笔记
  3. 画图软件:Xmind(思维导图) diagrams(流程图)

提升C内功--函数栈帧的创建和销毁(动画讲解)_第1张图片

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持博主,如有不足还请指点,博主及时改正

函数栈帧

文章目录

    • 函数栈帧
      • 1.预备知识点
        • 1.1 什么是函数栈帧
        • 1.2函数栈帧能解决什么问题呢?
        • 1.3相关寄存器和汇编指令
      • 2.函数之间的调用关系(谁调用了main函数)
      • 3.函数调用的过程
        • 3.1.main函数 函数栈帧的创建
        • 3.2.main函数核心代码执行
        • 3.3函数传参,调用函数
        • 3.4.Add函数 函数栈帧的创建
        • 3.5.Add函数核心代码执行
        • 3.6.Add函数栈帧的销毁
        • 3.7.main函数的剩余代码
        • 3.8.main函数 函数栈帧的销毁

1.预备知识点

1.1 什么是函数栈帧

  我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。

那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:

  • 函数参数函数返回值

  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)

  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

 

1.2函数栈帧能解决什么问题呢?

  1. 局部变量是如何创建的?

  2. 为什么局部变量不初始化内容是随机的?

  3. 函数调用时参数时如何传递的?传参的顺序是怎样的?

  4. 函数的形参和实参分别是怎样实例化的?

  5. 函数的返回值是如何带会的?

 

1.3相关寄存器和汇编指令

寄存器:

eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址

 

指令:

mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令

 这里不理解没有任何关系,我们在下面会逐句指令,进行相关解释。

2.函数之间的调用关系(谁调用了main函数)

  以下都以该程序代码来讲解内容。环境:visual stdio2013

#include 

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

int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

 

  大家都知道上述代码中,main函数会调用Add函数,那请问main函数会不会也是某个函数调用的呢?确实如此

  按F10将程序调试起来,并且将代码跑到Add函数的内部,查看调用堆栈的窗口。

提升C内功--函数栈帧的创建和销毁(动画讲解)_第2张图片

 

提升C内功--函数栈帧的创建和销毁(动画讲解)_第3张图片

 其中下面的函数先被调用,我们不难发现,Add是被main函数调用,并且main函数是被__tmainCRTStartup函数所调用的,而且__tmainCRTStartup函数是被mainCRTStarup函数调用的。这就是该程序函数调用的关系。

 

3.函数调用的过程

  接下来咱们会从汇编语言的角度,带着同学们逐步分析函数调用的过程,以及解释一些对我们理解比较重要的指令和寄存器。

F10调式起来,进入反汇编。

提升C内功--函数栈帧的创建和销毁(动画讲解)_第4张图片

 

3.1.main函数 函数栈帧的创建

espebp是维护函数栈帧的两个寄存器,esp–存放栈顶指针,ebp–存放栈底指针

提示:栈是向低地址处生长的,这也是为什么压栈,越要-esp的值,因为在向低地址处生长。

00FD1410  push        ebp  //1.将ebp的内容压栈,esp-4
00FD1411  mov         ebp,esp  //2.将esp的内容赋值给ebp
00FD1413  sub         esp,0E4h  //3.0E4h--十六进制的0E--十进制的228,即esp-228
00FD1419  push        ebx  //ebx压栈
00FD141A  push        esi  //esi压栈
00FD141B  push        edi  //edi压栈
    
00FD141C  lea         edi,[ebp-0E4h]  
00FD1422  mov         ecx,39h  
00FD1427  mov         eax,0CCCCCCCCh  
00FD142C  rep stos    dword ptr es:[edi]
//这四句指令的内容意思:
//将edi到ebp之间的内容全部修改成十六进制的CCCCCCCC,这也是烫烫烫烫的缘由

 

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第5张图片

 

3.2.main函数核心代码执行

	int a = 3;
00FD142E  mov         dword ptr [ebp-8],3 //ebp-8的内容放上3 ,a
	int b = 5;
00FD1435  mov         dword ptr [ebp-14h],5  //ebp-20的内容放上5,b
	int ret = 0;
00FD143C  mov         dword ptr [ebp-20h],0  //ebp-32的内容放上0,ret

 

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第6张图片

 

3.3函数传参,调用函数

	ret = Add(a, b);
//传参
00FD1443  mov         eax,dword ptr [ebp-14h]  //将ebp-20(b)的内容放进eax里面
00FD1446  push        eax  //将eax压栈
00FD1447  mov         ecx,dword ptr [ebp-8]  //将ebp-8(a)的内容放进ecx
00FD144A  push        ecx  //将ecx压栈
//调用Add函数
00FD144B  call        00FD10E1  //call指令
00FD1450  add         esp,8  //call指令的下一句指令

  call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

 

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第7张图片

 此时我们发现,函数传参的顺序是:从右到左进行传参

 

我们按F11,会跳转进下面的场景

_Add:
00FD10E1  jmp         00FD13C0  //转入Add函数,进行调用

此时我们再次按F11,即可跳进Add函数当中

 

3.4.Add函数 函数栈帧的创建

int Add(int x, int y)
{
int Add(int x, int y)
{
00FD13C0  push        ebp  
00FD13C1  mov         ebp,esp  
00FD13C3  sub         esp,0CCh  
00FD13C9  push        ebx  
00FD13CA  push        esi  
00FD13CB  push        edi  
    
00FD13CC  lea         edi,[ebp-0CCh]  
00FD13D2  mov         ecx,33h  
00FD13D7  mov         eax,0CCCCCCCCh  
00FD13DC  rep stos    dword ptr es:[edi] 
//栈帧的建立是类似的,大家直接看动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第8张图片

 

3.5.Add函数核心代码执行

	int z = 0;
00FD13DE  mov         dword ptr [ebp-8],0  //将ebp-8(z)的内容赋为0
	z = x + y;
00FD13E5  mov         eax,dword ptr [ebp+8]  //将ebp+8的内容(a)放进eax里
00FD13E8  add         eax,dword ptr [ebp+0Ch]  //将ebp+12的内容(b)加进eax里面
00FD13EB  mov         dword ptr [ebp-8],eax  //将eax的内容放进ebp-8(z)里面
	return z;
00FD13EE  mov         eax,dword ptr [ebp-8]  //将ebp-8的内容(z)放在eax里面
}

 

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第9张图片

3.6.Add函数栈帧的销毁

  通过上一步我们发现,在Add函数开始销毁之前,它将返回值先保存在了eax里面

00FD13F1  pop         edi  //edi出栈
00FD13F2  pop         esi  //esi出栈
00FD13F3  pop         ebx  //ebx出栈
00FD13F4  mov         esp,ebp  //将ebp的值赋值给esp
00FD13F6  pop         ebp  //ebp出栈
00FD13F7  ret  

 

  pop弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。

   ret:ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第10张图片

 

3.7.main函数的剩余代码

00FD1450  add         esp,8  
	ret = Add(a, b);
00FD1453  mov         dword ptr [ebp-20h],eax  //将eax的内容放进ebp-32的内容(ret)里面
    
//以下的功能大家可以不用琢磨,不影响我们理解函数栈帧
	printf("%d\n", ret);
00FD1456  mov         esi,esp  
00FD1458  mov         eax,dword ptr [ebp-20h]  
00FD145B  push        eax  
00FD145C  push        0FD5858h  
00FD1461  call        dword ptr ds:[00FD9114h]  
00FD1467  add         esp,8  //将两个函数参数出栈
00FD146A  cmp         esi,esp  
00FD146C  call        00FD113B  
	return 0;
00FD1471  xor         eax,eax  
}

提升C内功--函数栈帧的创建和销毁(动画讲解)_第11张图片

注意:call指令的下一句指令的地址,其实早已经在上一步的ret指令中就出栈了

 

3.8.main函数 函数栈帧的销毁

  同样也是与Add函数栈帧销毁类似

00FD1473  pop         edi  
00FD1474  pop         esi  
00FD1475  pop         ebx  
00FD1476  add         esp,0E4h  
00FD147C  cmp         ebp,esp  
00FD147E  call        00FD113B  
00FD1483  mov         esp,ebp  
00FD1485  pop         ebp  
00FD1486  ret

 

动画演示

提升C内功--函数栈帧的创建和销毁(动画讲解)_第12张图片

  main函数和Add函数的调用过程结束。

 

希望大家看了这篇文章对函数的调用能有不一样的理解,咱们下期间!

提升C内功--函数栈帧的创建和销毁(动画讲解)_第13张图片

你可能感兴趣的:(搞定C语言初阶,c语言,开发语言)