本文介绍了如下最简单的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,意思是“一次存两个寄存器”。

这里把 x29x30 保存到栈上。

其中:

x29 = 帧指针 frame pointer
x30 = 返回地址 link register

简单理解:

x29 负责记住当前函数的栈位置
x30 负责记住 main 执行完后该回到哪里

    add    x29, sp, #0x10

这句设置当前函数的帧指针:

x29 = sp + 0x10

从这里开始,编译器可以用 x29sp 来定位局部变量。


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 里,函数返回值通常放在 w0x0

前面 [sp + 4] 存的是 0。

所以这里是:

w0 = 0

对应:

return 0;

    ldp    x29, x30, [sp, #0x10]

恢复之前保存的 x29x30

也就是函数快结束了,把进入函数前的状态还原。


    add    sp, sp, #0x20

释放前面开辟的 32 字节栈空间。

前面是:

sub sp, sp, #0x20

现在反过来:

add sp, sp, #0x20

意思是:空间用完了,返还回去。


    ret

返回调用者,也就是 main 函数结束。


对照表

C++ 代码

汇编大概对应

int main()

main:

进入函数

sub sp...stp x29, x30...add x29...

int i = 10;

mov w8, #0xastr w8, [sp, #0x8]

cout << i

ldr w1...、准备 coutx0bl operator<<(int)

<< endl

准备 endl 地址、bl operator<<(...)

return 0;

ldr w0, [sp, #0x4]

离开函数

ldp x29, x30...add sp...ret


核心流程

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++ 一样,只是它把“变量、函数调用、返回值”全部拆成了寄存器和内存操作。

如果你觉得对你有帮助,不妨点个赞鼓励一下作者,愿你我共同进步!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐