C语言进阶 顶级神功! 函数栈帧的创建和销毁

Posted Cbiltps.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言进阶 顶级神功! 函数栈帧的创建和销毁相关的知识,希望对你有一定的参考价值。

温馨提示

大家好我是Cbiltps,在我的博客中如果有难以理解的句意难以用文字表达的重点,我会有配图。所以我的博客配图非常重要!!!

你想更好顺利的学习函数栈帧章节,我强烈建议你看我写的【C语言初阶】调试技巧的博客,会有什么效果?等你看了并学习本章后就会感受到!!!

更重要的一点,在分析流程的时候是根据反汇编语言一步一步的进行的,但是这个过程用文字不好解释(非常繁多),于是我为了理解画了很长时间的图,所有的一切都在图中展示!!!切记,切记!如果还不理解,请联系我,我们可以打视频等方式……

如果你对我感兴趣请看我的第一篇博客

开篇介绍

今天写的是【C语言进阶】的第二篇内容:函数栈帧,函数栈帧真的是修炼内功!

以前很多地方是讲函数栈帧的,但是绝大多数同学是听不懂的,后来就少讲或者是不讲;而且发现公司不愿意考这些东西了(考的少)。

但是这个东西非常重要,如果你真的懂了这个东西,那真的是练成了神功!

学前疑惑

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

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

如果你想明白这些问题,等你学习完函数栈帧的创建和销毁后就知道了,其实就是修炼了自己的内功,也能搞懂后期更多的知识!

学前准备

1. 环境选择

在学习函数栈帧的时候,我使用的环境是 VS2013VC6.0也是可以的,它对于函数栈帧的创建和销毁的过程是足够简单的),不要使用太高级的编译器,越高级的编译器,越不容易学习和观察(考虑各种问题,封装更加复杂)。

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

2. 知识铺垫

要想学习函数栈帧,就必须再给大家做一个小小的铺垫:了解寄存器!

数据寄存器:

  • eax
  • ebx
  • ecx
  • edx

指针寄存器:

  • ebp
  • esp

而本章节的重点是后指针寄存器(ebpesp,要想理解函数栈帧,就必须理解这两个寄存器。

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

ebpesp是如何维护函数栈帧的呢?
正在调用哪个函数,espebp维护的就是哪个函数的函数栈帧,espebp之间的空间就是为这个函数开辟的空间。

正文开始


1. 大致轮廓了解(源代码及反汇编)


本章节会用下面一段代码举例:

#include <stdio.h>

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

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;

	c = Add(a, b);

	printf("%d\\n", c);

	return 0;
}

首先调用堆栈观察:其实这个调用逻辑还是挺复杂的!

然后画一个概念图理解(栈区):

上面的解析只是一个大概的轮廓,但是具体是怎么做的呢?

我们需要转到反汇编来探究:

int main()
{
000919F0  push        ebp  
000919F1  mov         ebp,esp  
000919F3  sub         esp,0E4h  
000919F9  push        ebx  
000919FA  push        esi  
000919FB  push        edi  
000919FC  lea         edi,[ebp-0E4h]  
00091A02  mov         ecx,39h  
00091A07  mov         eax,0CCCCCCCCh  
00091A0C  rep stos    dword ptr es:[edi]  
	int a = 10;
00091A0E  mov         dword ptr [a],0Ah  
	int b = 20;
00091A15  mov         dword ptr [b],14h  
	int c = 0;
00091A1C  mov         dword ptr [c],0  

	c = Add(a, b);
00091A23  mov         eax,dword ptr [b]  
00091A26  push        eax  
00091A27  mov         ecx,dword ptr [a]  
00091A2A  push        ecx  
00091A2B  call        _Add (0911DBh)  
00091A30  add         esp,8  
00091A33  mov         dword ptr [c],eax  

	printf("%d\\n", c);
00091A36  mov         esi,esp  

	printf("%d\\n", c);
00091A38  mov         eax,dword ptr [c]  
00091A3B  push        eax  
00091A3C  push        95858h  
00091A41  call        dword ptr ds:[99114h]  
00091A47  add         esp,8  
00091A4A  cmp         esi,esp  
00091A4C  call        __RTC_CheckEsp (091136h)  

	return 0;
00091A51  xor         eax,eax  
}
00091A53  pop         edi  
00091A54  pop         esi  
00091A55  pop         ebx  
00091A56  add         esp,0E4h  
00091A5C  cmp         ebp,esp  
00091A5E  call        __RTC_CheckEsp (091136h)  
00091A63  mov         esp,ebp  
00091A65  pop         ebp  
00091A66  ret  

上面的代码就是反汇编代码,下面我们就开始学习吧!


2. 函数栈帧的创建和销毁总流程



3.函数栈帧的创建


3.1 main函数的创建(分解)

3.2 Add函数的创建(分解)


4.函数栈帧的销毁


4.1 main函数的销毁(分解)

5.1 Add函数的销毁(简洁)

前面的部分和main函数的一样,所以不做过多的介绍!


5. 问题回答


  • 局部变量是如何创建的?

首先为函数分配栈帧空间,在栈帧空间初始化一部分的空间后,在栈帧中给局部变量分配空间。

  • 为什么局部变量的值是随机值?


在栈帧空间初始化一部分的空间,其实就是赋了很多的'C',这就是随机值。
而以前打印出来看到的:烫烫烫烫烫烫烫烫烫烫烫烫…… 就是因为局部变量的值是随机值。

  • 函数是如何传参的?

直接看图:

  • 函数传参的顺序是怎样的?

其实还没有调用函数的时候,已经从右向左 push

  • 形参和实参是什么关系?

形参是实参的一分临时拷贝,值相同,空间独立。
改变形参,不会影响实参。

  • 函数调用是如何做的?

理解上面的图!不好解释!

  • 函数调用结束后是如何返回的?

理解上面的图!不好解释!

全文结束

以上是关于C语言进阶 顶级神功! 函数栈帧的创建和销毁的主要内容,如果未能解决你的问题,请参考以下文章

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

C语言深入逐汇编详解函数栈帧的创建和销毁过程

C语言学习 -- 函数栈帧的创建和销毁

函数栈帧的创建与销毁,带你了解代码底层原理

函数栈帧的创建与销毁