本章重点
- 了解汇编指令
- 深刻理解函数调用过程
样例代码:
#include <stdio.h> 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语言地址空间学习
- 代码段:存储程序的机器指令,包括函数的二进制代码。
- 字符常量区:存储字符串常量和字符常量,这些值在程序中是只读的,不可修改。
- 已初始化变量区:存储已经初始化的全局变量和静态变量。
- 未初始化变量区:存储未初始化的全局变量和静态变量,在程序加载时会被初始化为默认值(如0)。
- 堆:动态分配的内存区域,用于存储动态分配的变量、数据结构和对象。它的大小和位置在程序运行时动态调整。
- 栈:存储函数调用的局部变量、函数参数、函数返回地址以及其他与函数调用相关的信息。栈是一种后进先出(LIFO)的数据结构,函数调用时会在栈上创建一个新的帧,函数返回时会将该帧从栈中弹出。
认识相关寄存器
认识相关汇编命令
讲解思路图
备注: vs编译器有栈随机化的处理,所以每次看到的相关数据可能会不太一致,不过我们重点关注变化原理,弱化数据。
1、起步,main函数也是要被调用的
2、main函数也是要形成栈帧结构的
3、变量x和入栈
4、临时变量的入栈拷贝
6、开始调用函数
7、MyAdd函数栈帧形成
8、变量c入栈并完成加法
9、寄存器eax保存返回值
10、释放MyAdd函数栈帧
11、ret返回
12、函数参数的临时变量被销毁, 程序已经回到main函数栈帧,并且已经将寄存器eax的值给到z变量。
总结:
- 调用函数,需要先形成临时拷贝,形成过程是从右向左的
- 临时空间的开辟,是在对应函数栈帧内部开辟的
- 函数调用完毕,栈帧结构被释放掉
- 临时变量具有临时性的本质:栈帧具有临时性
- 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
- 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的
根据第六点,我们可以发现一个现象
#include <stdio.h> 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; }
我们上面的代码能很好的体现函数参数的地址位置关系,但这是一种不可预测和不可靠的行为。
临时变量的内存布局是由编译器决定的,这是未定义行为。不同的编译器可能采用不同的策略和规则来分配内存。因此,不能依赖于特定的内存布局来进行编程。
临时变量在栈上分配内存,通常是从高地址向低地址分配。因此,b的地址可能比a的地址更高。然而,这并不意味着b的地址正好位于a的下一个内存位置。