news 2026/3/27 4:56:00

函数调用ABI对比:arm64和x64从零实现示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数调用ABI对比:arm64和x64从零实现示例

深入函数调用的底层:arm64 与 x64 ABI 实战解析

你有没有遇到过这样的场景?一段 C 函数在 arm64 上运行正常,移植到 x64 却莫名其妙崩溃;或者调试时发现寄存器里的值完全不是预期的参数——这些问题的背后,往往藏着一个被忽视却至关重要的细节:ABI(应用二进制接口)

尤其是当你开始接触内联汇编、编写运行时系统、做逆向分析或开发 JIT 编译器时,如果不理解不同架构下函数是如何“握手”的,那就像蒙着眼睛开车。今天我们就以arm64 和 x64为例,从零出发,亲手写几行汇编代码,把两个主流架构的函数调用机制掰开揉碎讲清楚。


arm64 怎么调用函数?AAPCS64 规范全揭秘

ARM64,也叫 AArch64,是现代移动设备和越来越多服务器的心脏。它的函数调用规则由AAPCS64(ARM Architecture Procedure Call Standard for 64-bit)定义。这套标准不像某些文档那样晦涩难懂,其实逻辑非常清晰。

寄存器怎么分工?

在 arm64 中,每个寄存器都有明确的角色:

  • x0x7:前 8 个整型/指针参数走这里,返回值也放x0
  • x9x15:临时寄存器,调用者要用就得自己先保存
  • x19x29:被调用方必须保留的“稳定”寄存器
  • x30:链接寄存器(Link Register, LR),自动存返回地址
  • sp:栈指针,必须保持16 字节对齐

特别注意的是,arm64没有 push/pop 返回地址的操作,而是用一条bl指令直接跳转并把下一条指令地址写进x30。这不仅节省了内存访问,还让硬件更容易预测分支。

来看一个真实例子

我们实现一个简单的加法函数,看看它是如何工作的:

// arm64.s .global _start .text _start: mov x0, #10 // 第一个参数 mov x1, #20 // 第二个参数 bl add_numbers // 调用函数 → 自动将返回地址写入 x30 b . // 程序停止 add_numbers: add x2, x0, x1 // 计算 x0 + x1 mov x0, x2 // 结果放回 x0(返回值通道) ret // 默认从 x30 跳转回来

就这么几条指令,已经完整展示了 AAPCS64 的核心流程:

  1. 参数通过x0,x1传入;
  2. bl调用后,x30自动更新为_startb .的地址;
  3. 函数执行完用ret返回,本质是br x30
  4. 返回值仍在x0中可供后续使用。

整个过程干净利落,没有压栈弹栈,效率很高。

⚠️ 注意:如果这个函数内部还要调用其他函数,就必须手动保存x30,否则会被覆盖!


x64 又是怎么做的?System V ABI 解剖

相比之下,x64 更像是传统 CISC 架构的延续。它遵循的是System V ABI(Linux/macOS 使用的标准),虽然功能强大,但机制更复杂一些。

参数去哪儿了?

x64 把前六个整型参数分别放在这些寄存器里:

  • rdi,rsi,rdx,rcx,r8,r9

浮点数则走xmm0xmm7。超出的部分才通过栈传递。

返回值呢?整型放rax,浮点放xmm0

栈操作有何不同?

最明显的区别在于返回地址的处理方式:

  • call label→ 将下一条指令地址压入栈顶
  • ret→ 从栈顶弹出地址并跳转

这意味着 x64 的控制流依赖于栈的完整性。一旦栈被破坏,程序很可能直接 crash。

此外,x64 还有个“黑科技”:红区(Red Zone)

什么是红区?为什么重要?

在 x64 System V ABI 中,规定在当前栈指针(rsp)下方128 字节的区域是一个“禁区”,称为红区。这个区域内,被调用函数可以直接使用,而无需调整rsp

比如一个小函数只用了几个局部变量,总共不到 128 字节?那它根本不用sub rsp, xx,直接往[rsp - 8]写就行!省了一条指令,提升了性能。

但这招在信号处理或异常中断中要小心——因为异步进入可能踩到这块区域。

动手写一个 x64 版本

还是那个加法函数,我们来看看 x64 是怎么实现的:

# x64.s - System V ABI 示例 .section .text .globl _start _start: mov $10, %rdi # 参数1 → rdi mov $20, %rsi # 参数2 → rsi call add_numbers # call 会自动把返回地址压栈 jmp . # 停住 add_numbers: add %rdi, %rsi # rdi + rsi → rsi mov %rsi, %rax # 结果放入 rax(返回值) ret # 弹出栈中地址并返回

对比一下 arm64 的版本,你会发现:

  • 参数寄存器名字变了,数量少了两个;
  • 多了一个隐式的栈操作(call压栈);
  • 不需要额外保存返回地址,因为它已经在栈上了;
  • 当前示例没涉及本地变量,所以也没看到栈对齐或红区使用。

不过别忘了:x64 要求在每次call前,栈必须相对于该指令之后的位置 16 字节对齐。也就是说,如果你在调用前修改了rsp,一定要确保对齐。


arm64 vs x64:关键差异一览表

特性arm64 (AAPCS64)x64 (System V)
参数寄存器x0–x7(共8个)rdi, rsi, rdx, rcx, r8, r9(共6个)
返回值寄存器x0rax(整型) /xmm0(浮点)
返回地址存储存于x30(LR)压入栈中
调用指令bl funccall func
返回指令ret(等价于br x30ret(弹栈跳转)
栈对齐要求入口处 16 字节对齐call前 16 字节对齐
特有机制无红区支持 128 字节红区
被调用者需保存x19–x29,sp相关帧rbx,rbp,r12–r15
调用者需保存x9–x15r10,r11

💡 提示:Windows 下的 x64 ABI 和 System V 类似,但前四个参数是rcx,rdx,r8,r9,并且要求调用前预留32 字节“影子空间”(Shadow Space),即使不用也要留着。


实际开发中的坑点与秘籍

❌ 坑1:寄存器误用导致参数错乱

新手常犯的错误是在 arm64 中试图用x8传参,殊不知x8是用于间接跳转的临时寄存器,不属于参数通道。结果就是接收函数拿到的完全是垃圾数据。

✅ 正确做法:始终遵守x0–x7的顺序传参。

❌ 坑2:忽略栈对齐引发崩溃

尤其是在使用 SIMD 指令(如 NEON 或 AVX)时,未对齐的栈会导致bus errorsegmentation fault

例如,在 arm64 中进入函数后第一件事应该是检查 SP 是否 16 字节对齐:

and w8, sp, #15 cbz w8, 1f // 如果低4位为0,说明已对齐 sub sp, sp, w8 // 否则手动对齐(简化版) 1:

而在 x64 中,由于call本身会压入 8 字节返回地址,因此调用前的栈要是 16n+8 才能在call后变成 16n。

✅ 秘籍:善用红区提升小函数性能

假设你在写一个极短的辅助函数,只需要保存一两个局部变量,总共不超过 100 字节:

my_fast_func: mov [rsp - 8], rax ; 直接使用红区 ; ... 快速处理 ... ret ; 不动 rsp,不申请栈空间

这种技巧能让函数体更紧凑,减少指令数,在高频调用路径上效果显著。


什么时候必须关心 ABI?

虽然现代编译器都会自动生成符合 ABI 的代码,但在以下场景中,了解底层约定至关重要:

1. 编写内联汇编或纯汇编模块

无论是操作系统启动代码、上下文切换、协程调度,还是加密算法优化,只要涉及手写汇编,就必须严格遵循目标平台的 ABI。

2. 调试崩溃堆栈或 core dump

当你看到寄存器快照时,能否快速判断哪些是参数、哪个是返回地址、函数是否正在调用链中,全靠你对 ABI 的掌握。

比如看到x30 = 0x400abc,你就知道这是下一个返回目标;而看到rax = 0且刚从call返回,基本可以断定函数返回了 0。

3. 实现 FFI 或动态绑定

像 Python 的 ctypes、Rust 的 extern “C”、Java 的 JNI,都需要精确匹配参数布局和调用方式。跨平台时尤其要注意寄存器映射差异。

4. 开发 JIT 编译器或解释器

V8、LuaJIT、HotSpot 等项目都必须在运行时生成符合 ABI 的机器码。不了解参数如何传递,就无法正确调用原生函数。


写在最后:ABI 是软硬之间的桥梁

arm64 和 x64 的设计哲学在这里体现得淋漓尽致:

  • arm64 更现代、更规整:统一的寄存器命名、更多的参数通道、基于链接寄存器的高效跳转;
  • x64 更兼容、更灵活:继承 x86 的栈式调用模型,引入红区优化性能,兼顾历史代码平滑迁移。

两者各有千秋,但共同点是:都要求开发者尊重规则。哪怕只是一个小小的对齐偏差,也可能让程序在某个边缘场景突然崩塌。

随着 Apple Silicon 的普及、云原生对 ARM 服务器的支持增强,以及 RISC-V 的崛起,未来的系统开发必将面临更多架构间的协同挑战。掌握 arm64 和 x64 的 ABI 差异,不只是为了能看懂汇编,更是为了建立起一种“从硅到代码”的全局视角。

下次当你再看到blcall的时候,不妨多问一句:它背后到底发生了什么?也许答案,就藏在那几个不起眼的寄存器里。

如果你也在做跨平台底层开发,欢迎留言分享你的经验和踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/14 8:01:17

SGLang部署常见错误:host 0.0.0.0配置问题解决指南

SGLang部署常见错误:host 0.0.0.0配置问题解决指南 1. 引言 随着大语言模型(LLM)在各类业务场景中的广泛应用,高效、稳定的推理部署成为工程落地的关键环节。SGLang作为专为提升LLM推理性能而设计的框架,在优化吞吐量…

作者头像 李华
网站建设 2026/3/14 11:28:26

verl支持哪些LLM架构?主流模型兼容性测试

verl支持哪些LLM架构?主流模型兼容性测试 1. verl 介绍 verl 是一个灵活、高效且可用于生产环境的强化学习(RL)训练框架,专为大型语言模型(LLMs)的后训练设计。它由字节跳动火山引擎团队开源,…

作者头像 李华
网站建设 2026/3/26 2:05:23

BAAI/bge-m3性能测试:不同语言混合处理能力

BAAI/bge-m3性能测试:不同语言混合处理能力 1. 引言 1.1 多语言语义理解的技术背景 随着全球化信息流动的加速,跨语言、多语言内容处理已成为自然语言处理(NLP)领域的重要挑战。传统的语义相似度模型往往局限于单一语言环境&am…

作者头像 李华
网站建设 2026/3/12 2:39:14

看完就想试!通义千问2.5-7B打造的百万字长文档处理案例

看完就想试!通义千问2.5-7B打造的百万字长文档处理案例 1. 引言:为何选择通义千问2.5-7B-Instruct进行长文本处理? 在当前大模型应用场景中,长文档理解与生成能力已成为衡量模型实用性的关键指标。无论是法律合同分析、科研论文…

作者头像 李华
网站建设 2026/3/25 3:26:17

语音识别新体验:基于SenseVoice Small实现文字与情感事件标签同步识别

语音识别新体验:基于SenseVoice Small实现文字与情感事件标签同步识别 1. 引言 1.1 语音识别技术的演进与挑战 随着深度学习和大模型技术的发展,语音识别(ASR)已从传统的“语音转文字”逐步迈向多模态语义理解阶段。传统ASR系统…

作者头像 李华
网站建设 2026/3/13 15:24:42

智能会议记录实战:GLM-ASR-Nano-2512一键部署方案

智能会议记录实战:GLM-ASR-Nano-2512一键部署方案 1. 引言:智能语音识别的现实挑战与新选择 在现代企业办公场景中,会议记录是一项高频且耗时的任务。传统的人工转录方式效率低下,而市面上多数语音识别工具在面对复杂声学环境、…

作者头像 李华