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

简介: 前言我们都知道函数是个很神奇的东西,程序员可以自己写个函数完成对应的功能,但是这里有很多实现的小细节。具体我们来用一段短小精悍的代码来解释函数栈帧的创建和销毁。在理解的同时,我们要关注以下这几个问题:1.局部变量是怎么创建的?2.为什么局部变量的值是随机值?3.函数是怎么传参的?传参的顺序是怎么样的?4.形参和实参是什么关系?5.函数调用是怎么做的?6.函数调用是结束后怎么返回的?

前言

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

在理解的同时,我们要关注以下这几个问题:

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 <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;
}

2.1 main函数被谁调用?

我们一定要知道一个知识:每个函数调用都要创建自己的空间。

有了这句话我们就想知道main函数是不是也是被别的函数调用的呢?是的,main函数也是被调用的,我们用vs2022x86版本来看(不同的版本和不同的编译器其中的实现都有所不同)。

b20cbde8b480a00a9be570bd019e45e2.png

1e51c989cec216bbcbcf3a39441c4764.png

36d96d659f92029a7f43bd23cefc4c07.png

c461c8ad99e530bb7cbc01680ab8fca3.png

论:main函数是被其他函数调用的,具体是被invoke_main这个函数调用的,而这个invoke_main这个函数被_srct_common_main_seh这个函数调用的,这个_srct_common_main_seh函数是被_srct_common_main这个函数调用的。而这个_srct_common_main这个函数是被mainCRTStartup调用的。

这就搞清楚了main函数被调用的过程。再次理解了每个函数都是被调用的,都创建了自己的空间。

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

首先要知道这个知识点:函数的创建都是在栈上开辟空间的。

其次,函数既然是栈上开辟空间,那么如何利用呢?看图解释:

f758a90f70cb19a3845207086dc3252a.png

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

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

image.png

2.3.1 从main函数开始理解栈帧

这是main函数里面的反汇编代码:

image.png

3 问题问答

这张图就可以解决上述问题。

再来对上述问题进行总结:

1.局部变量是怎么创建的?

回答:就是把对应的数据放进寄存器中。


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

回答:在创建函数栈帧时,按照对应的设置个数,把栈顶指针指向的地址,向下按照对应的设置个数进行设置,设置为0xCCCCCCCC的值,所以我们看到有时候就是随机值的现象。

3.函数是怎么传参的?传参的顺序是怎么样的?

数据放进寄存器传参,传参的顺序是从右向左的。


4.形参和实参是什么关系?

形参就是实参的一份临时拷贝,用的寄存器存储数据,形参使用的时候直接找到寄存器中存储的数据直接使用。


5.函数调用是怎么做的?

首先开辟函数栈帧,然后进行相应的操作。


6.函数调用是结束后怎么返回的?

值是通过寄存器返回的。函数是通过ret指令返回到进入函数时记录的地址。

















相关文章
|
6月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
6月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
6月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(一)
C语言之反汇编查看函数栈帧的创建与销毁(一)
C语言之反汇编查看函数栈帧的创建与销毁(一)
|
3月前
|
存储 C语言
【C语言】——函数栈帧的创建与销毁
【C语言】——函数栈帧的创建与销毁
|
6月前
|
存储 编译器 C语言
C语言:底层剖析——函数栈帧的创建和销毁
C语言:底层剖析——函数栈帧的创建和销毁
|
6月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(二)
C语言之反汇编查看函数栈帧的创建与销毁(二)
|
11月前
|
编译器 C语言
函数栈帧的创建和销毁(以C语言代码为例,汇编代码的角度分析)(下)
函数栈帧的创建和销毁(以C语言代码为例,汇编代码的角度分析)
|
26天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
31 3
|
17天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
31 10
|
11天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。