【深入理解函数栈帧:探索函数调用的内部机制】

本章我们要介绍的不是数学中的函数,而是C语言中的函数哟!

【深入理解函数栈帧:探索函数调用的内部机制】_第1张图片

本章重点

  • 了解汇编指令
  • 深刻理解函数调用过程

样例代码:

#include 
int MyAdd(int a, int b)
{
	int c = 0;
	c = a + b;
	return c;
}

int main()
{
	int x = 0xA;
	int y = 0xB;
	int z = MyAdd(x, y);
	printf("z = %x\n", z);
	return 0;
}

C语言地址空间学习

【深入理解函数栈帧:探索函数调用的内部机制】_第2张图片

  • 代码段:存储程序的机器指令,包括函数的二进制代码。
  • 字符常量区:存储字符串常量和字符常量,这些值在程序中是只读的,不可修改。
  • 已初始化变量区:存储已经初始化的全局变量和静态变量。
  • 未初始化变量区:存储未初始化的全局变量和静态变量,在程序加载时会被初始化为默认值(如0)。
  • 堆:动态分配的内存区域,用于存储动态分配的变量、数据结构和对象。它的大小和位置在程序运行时动态调整。
  • 栈:存储函数调用的局部变量、函数参数、函数返回地址以及其他与函数调用相关的信息。栈是一种后进先出(LIFO)的数据结构,函数调用时会在栈上创建一个新的帧,函数返回时会将该帧从栈中弹出。

认识相关寄存器

  • eax:通用寄存器,保留临时数据,常用于返回值

  • ebx:通用寄存器,保留临时数据

  • ebp:栈底寄存器 esp:栈顶寄存器

  • eip:指令寄存器,保存当前指令的下一条指令的地址

认识相关汇编命令

  • mov:数据转移指令

  • push:数据入栈,同时esp栈顶寄存器也要发生改变

  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

  • sub:减法命令

  • add:加法命令

  • call:函数调用,1. 压入返回地址 2. 转入目标函数

  • jump:通过修改eip,转入目标函数,进行调用

  • ret:恢复返回地址,压入eip,类似pop eip命令

【深入理解函数栈帧:探索函数调用的内部机制】_第3张图片

讲解思路图

备注: vs编译器有栈随机化的处理,所以每次看到的相关数据可能会不太一致,不过我们重点关注变化原理,弱化数据。

1、起步,main函数也是要被调用的

【深入理解函数栈帧:探索函数调用的内部机制】_第4张图片

2、main函数也是要形成栈帧结构的

【深入理解函数栈帧:探索函数调用的内部机制】_第5张图片

【深入理解函数栈帧:探索函数调用的内部机制】_第6张图片

3、变量x和入栈

【深入理解函数栈帧:探索函数调用的内部机制】_第7张图片

4、临时变量的入栈拷贝

【深入理解函数栈帧:探索函数调用的内部机制】_第8张图片

【深入理解函数栈帧:探索函数调用的内部机制】_第9张图片

6、开始调用函数

【深入理解函数栈帧:探索函数调用的内部机制】_第10张图片

【深入理解函数栈帧:探索函数调用的内部机制】_第11张图片

【深入理解函数栈帧:探索函数调用的内部机制】_第12张图片

7、MyAdd函数栈帧形成

【深入理解函数栈帧:探索函数调用的内部机制】_第13张图片

【深入理解函数栈帧:探索函数调用的内部机制】_第14张图片

8、变量c入栈并完成加法

【深入理解函数栈帧:探索函数调用的内部机制】_第15张图片

9、寄存器eax保存返回值

【深入理解函数栈帧:探索函数调用的内部机制】_第16张图片

10、释放MyAdd函数栈帧

【深入理解函数栈帧:探索函数调用的内部机制】_第17张图片

11、ret返回

【深入理解函数栈帧:探索函数调用的内部机制】_第18张图片

12、函数参数的临时变量被销毁, 程序已经回到main函数栈帧,并且已经将寄存器eax的值给到z变量。

【深入理解函数栈帧:探索函数调用的内部机制】_第19张图片

总结:

  1. 调用函数,需要先形成临时拷贝,形成过程是从右向左的
  2. 临时空间的开辟,是在对应函数栈帧内部开辟的
  3. 函数调用完毕,栈帧结构被释放掉
  4. 临时变量具有临时性的本质:栈帧具有临时性
  5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
  6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的

根据第六点,我们可以发现一个现象

#include 
void MyAdd(int a, int b)
{
	//由于临时变量是从右往左创建的
	//所以b的地址高于a
	printf("before:%d\n", b);
	*(&a + 1) = 100;
	printf("after:%d\n", b);
}

int main()
{
	int x = 0xA;
	int y = 0xB;
	MyAdd(x, y);
	return 0;
}

【深入理解函数栈帧:探索函数调用的内部机制】_第20张图片

我们上面的代码能很好的体现函数参数的地址位置关系,但这是一种不可预测和不可靠的行为。

  1. 临时变量的内存布局是由编译器决定的,这是未定义行为。不同的编译器可能采用不同的策略和规则来分配内存。因此,不能依赖于特定的内存布局来进行编程。

  2. 临时变量在栈上分配内存,通常是从高地址向低地址分配。因此,b的地址可能比a的地址更高。然而,这并不意味着b的地址正好位于a的下一个内存位置。

【深入理解函数栈帧:探索函数调用的内部机制】_第21张图片

你可能感兴趣的:(深度理解C语言,c语言)