前言
在程序执行的过程中,每一行代码背后都隐藏着复杂的内存操作。理解函数调用栈如何管理这些变化,对于每一个程序员而言都是至关重要的技能。本文将结合vscode分析函数调用栈,揭示程序如何动态地管理内存,从而帮助我们更高效地调试和优化代码。本文主要结合了函数调用栈的相关博客和视频,并加入自己的理解。
0.注意
(1)本文的环境为windows+mingw gcc,使用的IDE为vscode。
(2)不同的编译器的汇编有所不同。
(3)本文严格按照汇编来理解函数调用的栈分布
1.相关寄存器简述
寄存器名称 | 寄存器描述 |
---|---|
eax | 主要用于算术运算和数据传输操作 |
ecx | 通常用于计数或传参 |
edx | 通常用于传递参数或辅助计算 |
rip | 存储着处理器将要执行的下一条指令的地址 |
rbp | 在函数调用中用作基址指针 |
rsp | 指示了堆栈的顶部位置 |
2.代码示例
#include<stdio.h>
int add(int a, int b){
return a+b;
}
int main(void){
int a = 1;
int b = 2;
int c = add(a,b);
printf("%d\n",c);
return 0;
}
3.结合vscode汇编图解
3.1 步骤1 主函数准备传入参数
main函数:
int a = 1;
int b = 2;
通过以上汇编可以看出,main栈中在rbp基地址高4字节和8字节的位置存储了a和b变量;
为了调用add函数,最终将a和b分别传入到ecx和edx寄存器
栈分布如下:
3.2 步骤2 保存现场
call调用会将rip推向栈,即将函数调用的下一个指令保存到栈中,以便函数返回时能接着往下执行指令。汇编如下。
栈分布如下:
3.3 步骤3 保存形参并计算
根据以下汇编,可以看出这几步骤主要讲main传递来的形参进行保存,然后取出进行计算。
栈分布如下:
3.4 步骤4 恢复现场
步骤3已经完成了数据的运算并将数据存储到eax中,是时候该返回数据给main函数了。
pop rbp:将栈顶弹出并赋值给rbp,这样rbp就恢复到main栈的rbp。
ret:将栈顶元素弹出并赋值给rip,因为此时栈顶为调用指令为步骤2中保存的调用指令下一个地址,所以此时rip就回到main函数中调用add函数后的指令,main函数得以继续向下执行。
返回汇编如下:
栈分布如下:
|
|
总结
每一次函数调用的背后,都有一个精心设计的内存舞台,等待着我们去探索和驾驭。