x64 与 ARM64:从寄存器到系统调用的真实世界对照课
你有没有在调试一段看似“完全正确”的跨平台 C 代码时,突然在 ARM64 上遇到SIGBUS,而 x64 上却运行如常?
或者,在写内联汇编处理网络包时,发现同样的mov指令在两种架构下行为迥异,甚至引发段错误?
又或者,看着objdump输出里一模一样的函数签名,却在反汇编中看到截然不同的寄存器使用逻辑,心里直犯嘀咕:“这到底是编译器偷懒,还是架构真有这么大差别?”
这不是玄学——这是底层计算范式的分水岭。x64 和 ARM64 不是同一套逻辑的两个方言,而是两套独立演化的、带着各自历史包袱与设计哲学的指令集宇宙。它们共享“64位”这个标签,但就像汉语和西班牙语都用“字”来指代书写单位,实际语法、语序、隐含规则天差地别。
这篇文章不讲手册复读,不列参数表格,也不堆砌术语。它是一份基于真实调试经验、编译器输出和内核源码交叉验证的对照笔记——专为正在啃《ARM Architecture Reference Manual》却卡在第3章、或刚被 GCC 的-mgeneral-regs-only救了一命的开发者所写。
我们直接从最痛的五个点切入:寄存器怎么用、函数怎么调、内存怎么对、异常怎么进、栈帧怎么长。每一个点,都配以你能在终端里立刻敲出来验证的代码、反汇编片段,以及一句“坦白说,我第一次踩坑时也这么想”。
寄存器:不是数量多就赢,是“谁该管什么”才致命
先抛开“ARM64 有 31 个通用寄存器,x64 只有 16 个”这种教科书式对比。真正让你半夜改 bug 的,是下面这两行汇编:
# x64 (gcc -O0) mov %rdi,%rax add %rsi,%rax # ARM64 (aarch64-gcc -O0) add x0, x0, x1表面看都是“把第一个参数加第二个参数”,但背后藏着三重认知断层:
第一层:命名即契约
- x64 的
RAX是一个“多面手”:它既是累加器(ADD RAX, RBX),又是返回值寄存器(ret后调用者直接读RAX),还是乘法高位结果存放处(MUL RBX→ 高64位进RDX,低64位进RAX)。你写mov eax, 1,CPU 会默默清零RAX高32位——这是 ABI 规范,不是硬件特性。 - ARM64 的
X0就是X0:它只负责传参/返值,没有隐藏职责。XZR(X Zero Register)是物理存在的零寄存器,mov x0, xzr是一条单周期指令;而 x64 要清零,得用xor rax, rax(依赖标志位)或mov rax, 0(立即数编码更长)。初学者常误以为mov eax, 0在 x64 上等价于mov x0, xzr,其实前者可能触发部分寄存器重命名开销,后者是纯硬件零源。
第二层:调用者 vs 被调用者,谁背锅?
- x64 的 System V ABI 明确划分:
RBX,RBP,R12–R15是 callee-saved(被调用者必须保存/恢复),RDI–R9是 caller-saved(调用者自己负责)。这意味着:如果你写一个裸汇编函数,用了R12却没push r12/pop r12,调用你的 C 函数很可能崩溃——因为编译器默认R12的值在函数返回后不变。 - ARM64 的 AAPCS64