c语言分层理解(函数栈帧的创建和销毁)

文章目录

  • 前言
  • 正题
    • 1.了解寄存器
      • 1.1 寄存器是什么?
      • 1.2 常见的寄存器
      • 1.3 什么是栈,什么是栈帧?
        • 1.3.1 什么是栈?
        • 1.3.2 什么是栈帧?
      • 1.4 常见的反汇编指令
    • 2.探讨函数栈帧的创建和销毁
      • 2.1 main函数被谁调用?
      • 2.2 怎么在栈区开辟空间以及如何利用空间?
      • 2.3 汇编角度深入函数栈帧的创建和销毁
        • 2.3.1 从main函数开始理解栈帧
    • 3 问题问答

前言

我们都知道函数是个很神奇的东西,程序员可以自己写个函数完成对应的功能,但是这里有很多实现的小细节。具体我们来用一段短小精悍的代码来解释函数栈帧的创建和销毁。
在理解的同时,我们要关注以下这几个问题:

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

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

正题

1.了解寄存器

1.1 寄存器是什么?

寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。详细一点就是寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。

1.2 常见的寄存器

4个数据寄存器(eax、ebx、ecx、edx)
2个变址和指针寄存器(esi、edi)
2个指针寄存器(esp、ebp)
esp:寄存器存放当前线程的栈顶指针
ebp:寄存器存放当前线程的栈底指针
esp和ebp这两个寄存器中存放地址,这两个地址用来维护函数栈帧

具体寄存器知识还有很多,这里我们先做了解。

1.3 什么是栈,什么是栈帧?

1.3.1 什么是栈?

在数据结构中,栈是限定仅在表尾进行插入或删除操作的线性表。栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
在计算机系统中,栈也可以称之为栈内存是一个具有动态内存区域,存储函数内部(包括main函数)的局部变量和方法调用和函数参数值,是由系统自动分配的,一般速度较快;存储地址是连续且存在有限栈容量,会出现溢出现象程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。

1.3.2 什么是栈帧?

每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame).每个独立的栈帧一般包括:

  1. 函数的返回地址和参数
  2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  3. 函数调用的上下文

栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围,ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部。

1.4 常见的反汇编指令

add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数

sub:减法指令,格式和add格式一样

call:调用函数,一般函数的参数放在寄存器中

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

push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),也就是说,push指令一旦下达,esp的指针偏移量就会减4

pop:与push相反,esp每次加4(字节),一个数据出栈

mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的一份

lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。(lea就是load effective address的意思)

stos:串行存储指令,它实现把eax中的数据放入到edi所指的地址中,同时edi后移4个字节

jmp:无条件跳转指令,对应于大量的条件跳转指令

cmp:进行比较两个操作数的大小,为第一个操作减去第二个操作数,但不影响第两个操作数的值

以上知识目前只了解,待我能力杠杠的时候,会深度去探讨和研究这些汇编和底层的东西,现在只需要知道即可。

上面的准备知识已经完毕,下面我们一起来通过代码调试到汇编,从汇编的角度去看问题。

2.探讨函数栈帧的创建和销毁

首先展示代码:

#include 
//观察函数栈帧
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;
}

2.1 main函数被谁调用?

我们一定要知道一个知识:每个函数调用都要创建自己的空间。
有了这句话我们就想知道main函数是不是也是被别的函数调用的呢?是的,main函数也是被调用的,我们用vs2022x86版本来看(不同的版本和不同的编译器其中的实现都有所不同)。
c语言分层理解(函数栈帧的创建和销毁)_第1张图片
c语言分层理解(函数栈帧的创建和销毁)_第2张图片
c语言分层理解(函数栈帧的创建和销毁)_第3张图片
c语言分层理解(函数栈帧的创建和销毁)_第4张图片
结论:main函数是被其他函数调用的,具体是被invoke_main这个函数调用的,而这个invoke_main这个函数被_srct_common_main_seh这个函数调用的,这个_srct_common_main_seh函数是被_srct_common_main这个函数调用的。而这个_srct_common_main这个函数是被mainCRTStartup调用的。
这就搞清楚了main函数被调用的过程。再次理解了每个函数都是被调用的,都创建了自己的空间。

2.2 怎么在栈区开辟空间以及如何利用空间?

首先要知道这个知识点:函数的创建都是在栈上开辟空间的。
其次,函数既然是栈上开辟空间,那么如何利用呢?看图解释:
c语言分层理解(函数栈帧的创建和销毁)_第5张图片

2.3 汇编角度深入函数栈帧的创建和销毁

在x86情况下,调试起来看这段代码的整个流程:

2.3.1 从main函数开始理解栈帧

这是main函数里面的反汇编代码:
c语言分层理解(函数栈帧的创建和销毁)_第6张图片

3 问题问答

这张图就可以解决上述问题。
再来对上述问题进行总结:

1.局部变量是怎么创建的?
回答:就是把对应的数据放进寄存器中。

2.为什么局部变量不初始化的值是随机值?
回答:在创建函数栈帧时,按照对应的设置个数,把栈顶指针指向的地址,向下按照对应的设置个数进行设置,设置为0xCCCCCCCC的值,所以我们看到有时候就是随机值的现象。

3.函数是怎么传参的?传参的顺序是怎么样的?
数据放进寄存器传参,传参的顺序是从右向左的。

4.形参和实参是什么关系?
形参就是实参的一份临时拷贝,用的寄存器存储数据,形参使用的时候直接找到寄存器中存储的数据直接使用。

5.函数调用是怎么做的?
首先开辟函数栈帧,然后进行相应的操作。

6.函数调用是结束后怎么返回的?
值是通过寄存器返回的。函数是通过ret指令返回到进入函数时记录的地址。

感谢支持,谢谢你们的陪伴,我们一起加油!

你可能感兴趣的:(c语言学习,c语言,开发语言)