ARM64 系统调用 ABI

本页目录

AArch64 普通函数调用和 Linux 系统调用都从 x0 开始传参,但它们不是同一套约定。普通函数用 bl 跳转并按 AAPCS64 分配寄存器;系统调用把调用号放进 w8,执行 svc #0,由内核完成请求。svc 的立即数不是 Linux 系统调用号。

本文讨论 Linux 原生 AArch64 用户态,不包含 AArch32 兼容 ABI。应用应使用目标平台头文件中的 SYS_* 或 _NR*,不要把 x86-64 的调用号带到 ARM64。

先掌握应用可见的约定

用途AArch64 Linux 约定
进入内核svc #0
系统调用号w8,即 x8 的低 32 位
最多六个参数x0、x1、x2、x3、x4、x5
主要返回值x0
原始错误x0 中的 -errno,错误范围 -4095 到 -1
libc 公共封装的错误通常返回 -1,并设置线程局部 errno

同一个 x0 在入口表示参数 1,出口表示结果。例如 write 入口的 x0 是文件描述符,成功出口的 x0 是本次写入的字节数。syscall() 这个 C 函数也已经处理 errno,不等于一段直接返回原始寄存器的汇编。syscall(2)

写 w0 会清零高 32 位;宽度必须跟随参数类型 mov w0, #-1 x0 = 0x00000000ffffffff 写 32 位视图后的 64 位寄存器值 sxtw x0, w0 x0 = 0xffffffffffffffff 把有符号 32 位 -1 扩展为 64 位 -1 内核参数若是 int,按 int 解释低位;若是 long / 指针,需要完整宽度。 两种位模式对某些 int 参数效果相同,不代表对所有系统调用参数都可互换。

w 与 x 是同一个寄存器的不同宽度视图

写 w 寄存器会把对应 x 寄存器高 32 位清零,所以 mov w0, #-1 得到的完整 x0 值不是 64 位的 -1。若内核参数类型是 int,低 32 位的全 1 可以按该类型解释成 -1;若接口要求有符号 64 位数,就不能省略符号扩展。指针、size_t 和偏移参数也必须按目标 ABI 与接口定义准备,不能统一用 w 寄存器装载。Arm AArch64 寄存器

下面的位运算模型只说明寄存器位模式,不调用内核。它同时演示把返回值截成 32 位会掩盖完整 64 位结果,实际原始错误判断必须使用 x0 的宽度。

MASK32 = (1 << 32) - 1
MASK64 = (1 << 64) - 1
w_minus_one = (-1) & MASK32
x_after_w_write = w_minus_one
x_after_sxtw = (w_minus_one | (~MASK32)) & MASK64
assert x_after_w_write == 0x00000000ffffffff
assert x_after_sxtw == 0xffffffffffffffff

def raw_error(bits):
    return (bits & MASK64) >= ((-4095) & MASK64)

assert raw_error(x_after_sxtw)
assert not raw_error(x_after_w_write)
assert raw_error((-22) & MASK64)
assert not raw_error(0x0000000100000000)
assert (0x0000000100000000 & MASK32) == 0

这里 x_after_w_write 不在 64 位错误区间,不能把低 32 位的 0xffffffff 看到就判为系统调用失败。另一方面,也不能因为同一个数不在错误区间,就宣布它是有效地址:是否为地址以及是否可访问仍由具体调用结果和映射条件决定。

同样的低32位,为什么不是同一个返回值?

固定输入 -1 切换扩展方式,两块十六进制中只有高半部改变,64位含义却从 4294967295 变成 -1。再选择 -4096:即使符号扩展后为负,也不属于 [-4095,-1] 的原始错误编码区间。

正在呈现知识画面
同样的低32位,为什么不是同一个返回值?

写 w 寄存器清零高32位,符号扩展则复制符号位。只有完整64位落入保留错误区间,才按 -errno 解释。

SVC 到底保存什么

AArch64 EL0 → EL1:异常状态由硬件保存,通用寄存器由内核保存 用户执行 svc #0 w8 是系统调用号,x0–x5 是参数 1 CPU 记录异常状态 返回 PC → ELR_EL1;PSTATE → SPSR_EL1 2 进入同步异常向量 选择 EL1 栈;ESR_EL1 标识 SVC 异常 3 Linux 保存寄存器并分派 形成 pt_regs,检查追踪等工作,调用处理函数 4 Linux 恢复上下文并执行 eret x0 带回结果,返回用户态 5 图以 EL1 内核说明架构机制;虚拟化配置不改变应用使用的 syscall ABI。

以从 EL0 进入 EL1 为例,硬件保存返回地址与处理器状态,选择目标异常级的栈,并进入异常向量。来自较低异常级、AArch64 状态的同步异常槽位位于 VBAR_EL1 + 0x400;入口代码还需要根据异常原因区分 SVC、指令异常等。

硬件没有一次性把 x0–x30 全压入内存。Linux 软件入口负责保存所需通用寄存器、用户栈指针等,形成可供追踪、信号递送和返回路径操作的上下文。不要将“硬件没有自动保存所有通用寄存器”误写成“硬件不保存任何状态”。Arm 异常模型

阅读 Linux v6.18 时,可以从 entry.S 的 kernel_entry/kernel_exit 跟到 syscall.c 的 el0_svc_common 和 invoke_syscall。进入与退出还涉及追踪、信号、重调度及安全相关工作,不能用几条拼接的伪汇编代表完整内核实现。入口汇编、系统调用分派

与 x86-64 对照时,比较同一层

问题原生 x86-64 Linux原生 AArch64 Linux
参数 4r10,避开硬件使用的 rcxx3
返回位置由硬件暂存到哪里rcxELR_EL1(以 EL1 为例)
处理器状态由硬件暂存到哪里r11SPSR_EL1(以 EL1 为例)
建立入口内存帧Linux 软件保存寄存器Linux 软件保存寄存器
异常返回满足条件用 SYSRET,否则 IRETERET

AArch64 不存在 x86 SYSCALL 对 rcx/r11 的那种同名通用寄存器破坏,仍不能据此说“系统调用保留所有寄存器状态”。尤其是扩展向量状态有独立 ABI:SVE 的系统调用边界对高位向量、谓词寄存器等有明确约定,不能假定整套 SVE/SME 状态保持不变。Linux SVE ABI

性能也不能从表中推导。是否触发页表切换、内核缓解措施、CPU 型号、频率和虚拟化环境都会影响延迟,寄存器排列更整齐不等于必然更快。

一个可组装的完整例子

下面只依赖 AArch64 Linux 的 write=64、exit=93。它处理短写与 EINTR,其他失败退出;适合观察 ABI,不是通用异步 I/O 循环。

.section .rodata
message:
    .ascii "hello\n"

.section .text
.global _start
_start:
    adrp x1, message
    add x1, x1, :lo12:message
    mov x2, #6
.Lwrite:
    mov x0, #1
    mov w8, #64
    svc #0
    cmn x0, #4              // -EINTR
    b.eq .Lwrite
    cmp x0, #0
    b.le .Lerror
    add x1, x1, x0
    subs x2, x2, x0
    b.ne .Lwrite
    mov x0, #0
    b .Lexit
.Lerror:
    mov x0, #1
.Lexit:
    mov w8, #93             // 原始 exit 退出当前线程
    svc #0
.section .note.GNU-stack,"",%progbits

在 AArch64 Linux 上可用本机 as、ld 构建;在其他架构上可只做交叉组装和链接:

clang --target=aarch64-linux-gnu -c syscall-arm64.S -o syscall-arm64.o
ld.lld -m aarch64elf -o syscall-arm64 syscall-arm64.o
readelf -h -l syscall-arm64

生成目标文件不代表已在 ARM64 上执行验证。运行还需要 AArch64 Linux 或正确配置的模拟环境。此例没有 libc 启动代码,exit 也不负责刷新 stdio 缓冲。

让汇编叶子函数同时满足 C ABI 和 syscall ABI

前面的 _start 没有普通调用者。若想让 C 调用一个汇编封装,函数既要按 AAPCS64 接收参数与返回,也要在 svc 前放好 Linux 所需寄存器。以下 raw-write-arm64.S 把 C 的三个参数原样用于 write,直接返回原始结果。

.text
.global raw_write_arm64
.type raw_write_arm64, %function
raw_write_arm64:
    mov w8, #64
    svc #0
    ret
.size raw_write_arm64, .-raw_write_arm64
.section .note.GNU-stack,"",%progbits

对应的 C 调用者保存为 raw-write-main.c:

extern long raw_write_arm64(int fd, const void *buffer, unsigned long size);
int main(void) {
    static const char message[] = "hello\n";
    const char *next = message;
    unsigned long left = sizeof message - 1;
    while (left != 0) {
        long result = raw_write_arm64(1, next, left);
        if (result == -4) continue;  /* 原生 Linux 的 EINTR */
        if (result <= 0) return 1;
        next += result;
        left -= (unsigned long)result;
    }
    return 0;
}

这里 C 调用把 fd、buffer、size 放到 x0–x2 对应的参数位置,汇编只需设置 w8。bl 建立的返回地址在 x30;svc 的异常返回状态保存在异常级寄存器中,Linux 返回用户态后,此叶子封装可以 ret 回 C 调用者。

如果以后在封装中新增 bl helper,就必须保存并恢复自己的返回地址,并遵守被调用者保存寄存器规则。AAPCS64 的公共接口要求 SP 按 16 字节对齐;不要仿照只压一个 8 字节值的写法,让后续调用在错误的栈对齐下执行。AAPCS64 子程序调用

clang --target=aarch64-linux-gnu -c raw-write-arm64.S -o raw-write-arm64.o
clang --target=aarch64-linux-gnu -ffreestanding -c raw-write-main.c -o raw-write-main.o
readelf -sW raw-write-arm64.o
readelf -rW raw-write-main.o

以上两次编译无需 C 标准库头文件,适合在其他架构检查符号和调用重定位。它们并未完成 C 运行时链接。实际运行时,在 AArch64 Linux 使用本机工具链执行 cc raw-write-main.c raw-write-arm64.S -o raw-write-demo,再运行 ./raw-write-demo;预期输出 hello。

检查层次能说明什么不能替代什么
交叉组装指令与重定位语法可接受内核是否接受本次参数
ELF header / symbol / relocation目标是 AArch64,调用引用已生成运行时寄存器内容正确
本机或模拟环境运行当前路径的输出与退出状态所有错误路径、真实硬件性能
目标机压力与故障测试信号、短写等条件下的行为其他内核配置的普遍保证

CPU 特性和系统调用号是不同问题

getcpu、mlock2 并非 ARM64 独有系统调用。PR_PAC_RESET_KEYS 是 prctl 的一种操作,不是独立 syscall 编号。PAC、BTI、SVE、LSE 是否可用,需要结合处理器能力与内核暴露的 ABI 判断。

在 AArch64 Linux C 程序中,使用系统头文件中的位定义,不手抄 HWCAP 常量:

#include <sys/auxv.h>
#include <asm/hwcap.h>

int has_lse_atomics(void)
{
    return (getauxval(AT_HWCAP) & HWCAP_ATOMICS) != 0;
}

HWCAP 描述内核允许用户态使用的能力,比根据 CPU 型号字符串猜测可靠;它不是系统调用是否实现的清单。Linux ELF HWCAP

LDXR/STXR 是用户态原子算法可能使用的指令,不需要每次进入内核。它们与带 acquire/release 语义的 LDAXR/STLXR 要区分;LSE 则提供另一组原子指令。原子性、内存顺序和等待唤醒是三个问题,不能从“没有 x86 lock 前缀”推导 NUMA 扩展性。实际同步如何进入内核,继续看 futex。