本章我们要介绍的不是数学中的函数,而是C语言中的函数哟!
本章重点
- 了解汇编指令
- 深刻理解函数调用过程
样例代码:
#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)的数据结构,函数调用时会在栈上创建一个新的帧,函数返回时会将该帧从栈中弹出。
认识相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器 esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
认识相关汇编命令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
讲解思路图
备注: 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
的下一个内存位置。