main函数栈帧的理解(C++代码为例)
本文介绍了如下最简单的main函数:
#include <iostream>
using namespace std;
int main() {
int i=10;
std::cout<<i<<endl;
return 0;
}
反汇编窗口如下:

解释如下:
下面这段是 ARM64 / AArch64 汇编,不是 x86 汇编,机器是 Apple M5芯片,所以 C++ 被编译成了 ARM64 指令。
C++ 代码是:
int main() {
int i = 10;
std::cout << i << endl;
return 0;
}
它对应的汇编大概可以分成 5 段:开栈帧 → 初始化局部变量 → 调用 cout 输出 i → 调用 endl → 返回 0。
1. 函数开始:准备栈空间
main:
sub sp, sp, #0x20
sp 是 栈指针。
这句的意思是:
sp = sp - 0x20
0x20 是十六进制,也就是十进制的 32。
所以这句是在栈上开辟 32 字节空间,给 main 函数临时使用。
stp x29, x30, [sp, #0x10]
stp 是 store pair,意思是“一次存两个寄存器”。
这里把 x29 和 x30 保存到栈上。
其中:
x29 = 帧指针 frame pointer
x30 = 返回地址 link register
简单理解:
x29 负责记住当前函数的栈位置
x30 负责记住 main 执行完后该回到哪里
add x29, sp, #0x10
这句设置当前函数的帧指针:
x29 = sp + 0x10
从这里开始,编译器可以用 x29 或 sp 来定位局部变量。
2. 准备 return 0
mov w8, #0x0
str w8, [sp, #0x4]
mov w8, #0x0:
把 0 放进 w8
str w8, [sp, #0x4]:
把 w8 里的 0 存到栈上 sp + 4 的位置
这其实是在提前准备:
return 0;
为什么不用最后才准备?
因为未优化编译时,编译器经常会生成比较晦涩的代码:先把返回值放在某个临时位置,最后再读回来。
3. 初始化局部变量 i
stur wzr, [x29, #-0x4]
wzr 是一个特殊寄存器,永远表示 0。
这句大概是在把某个栈位置清零。很多时候这是调试/未优化模式下生成的“初始化痕迹”,对应函数栈上的某个临时槽位。
mov w8, #0xa
0xa 是十六进制,也就是十进制的 10。
所以这句是:
w8 = 10
对应 C++:
int i = 10;
str w8, [sp, #0x8]
把 w8 里的 10 存到栈上:
内存[sp + 8] = 10
这就是把局部变量 i 存到了栈里。
可以理解成:
int i = 10;
在汇编层面大概变成了:
拿一个寄存器 w8
把 10 放进去
再把 10 存到栈上的 i 的位置
4. 准备调用
cout << i
ldr w1, [sp, #0x8]
ldr 是 load register,从内存读到寄存器。
这句是:
w1 = 内存[sp + 8]
前面 sp + 8 存的是 i,也就是 10。
所以现在:
w1 = 10
在 ARM64 调用约定里,函数参数通常放在:
x0/w0, x1/w1, x2/w2, ...
因为 operator<<(int) 需要一个整数参数,所以 i 被放进了 w1。
adrp x0, 4
ldr x0, [x0, #0x28]
这两句是在准备 std::cout 的地址。
cout << i 本质上会变成类似:
std::cout.operator<<(i);
或者更准确地说,是调用 operator<< 函数。
调用这个函数时,需要两个东西:
第一个参数:cout 对象
第二个参数:i,也就是 10
所以这里:
x0 = std::cout 的地址
w1 = 10
bl 0x10247c680
bl 是 branch with link,意思是“跳过去执行一个函数,并记住回来地址”。
这里调用的是:
std::ostream::operator<<(int)
执行:
std::cout << i
调用结束后,x0 通常会保存返回值。
而 cout << i 的返回值其实还是 cout 自己,这样才能继续链式调用:
std::cout << i << endl;
这就是为什么 C++ 可以连续写两个 <<。
5. 调用
endl
adrp x1, 0
add x1, x1, #0x510
这两句是在准备 endl 函数的地址。
endl 不是普通字符串,它是一个函数模板实例。它的作用大致是:
输出换行符
刷新缓冲区
所以:
cout << endl;
其实是把 endl 这个函数指针传给 operator<<。
bl 0x10247c4e4
这里调用的是另一个版本的 operator<<:
operator<<(std::ostream& (*)(std::ostream&))
也就是专门处理 endl 这种“操纵器函数”的版本。
所以这句对应:
std::cout << endl;
6. 准备返回值并退出函数
ldr w0, [sp, #0x4]
ARM64 里,函数返回值通常放在 w0 或 x0。
前面 [sp + 4] 存的是 0。
所以这里是:
w0 = 0
对应:
return 0;
ldp x29, x30, [sp, #0x10]
恢复之前保存的 x29 和 x30。
也就是函数快结束了,把进入函数前的状态还原。
add sp, sp, #0x20
释放前面开辟的 32 字节栈空间。
前面是:
sub sp, sp, #0x20
现在反过来:
add sp, sp, #0x20
意思是:空间用完了,返还回去。
ret
返回调用者,也就是 main 函数结束。
对照表
|
C++ 代码 |
汇编大概对应 |
|---|---|
|
|
|
|
进入函数 |
|
|
|
|
|
|
|
|
|
准备 |
|
|
|
|
离开函数 |
|
核心流程
int i = 10;
std::cout << i << endl;
return 0;
在汇编里流程如下:
1. 给 main 准备栈空间
2. 把数字 10 存到栈上的变量 i 里
3. 把 i 读出来,放到参数寄存器 w1
4. 把 std::cout 的地址放到参数寄存器 x0
5. 调用 cout 输出 int 的函数
6. 准备 endl 的函数地址
7. 调用 cout 输出 endl 的函数
8. 把 0 放到返回值寄存器 w0
9. 恢复栈,并返回

汇编代码做的事情和 C++ 一样,只是它把“变量、函数调用、返回值”全部拆成了寄存器和内存操作。
如果你觉得对你有帮助,不妨点个赞鼓励一下作者,愿你我共同进步!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)