---
title: Linux x86-64 系统调用 ABI：寄存器、SYSCALL 与 RCX/R11
url: https://doc.liz6.com/systems-programming/02-system-call-abi/01-x86-64-system-call-abi
locale: zh
area: systems-programming
tags:
- x86-64
- syscall
- ABI
- Linux
- assembly
- vDSO
- systems-programming
- 系统调用ABI
date: 2026-08-11
modified: 2026-08-11
description: 从 CPU 指令语义、Linux syscall ABI 和当前内核 entry_SYSCALL_64 三层解释 x86-64 系统调用，重点说明 r10 参数、RCX/R11 clobber、栈切换、red zone、SYSRET 与 vDSO。
---

# Linux x86-64 系统调用 ABI

写手工汇编、inline asm、调试 seccomp 或阅读反汇编时，最容易混淆的是三套不同规则：

1. **System V AMD64 C ABI**：普通用户态函数怎样传参、返回和保存寄存器。
2. **Linux x86-64 syscall ABI**：用户态怎样把系统调用号和最多 6 个参数交给内核。
3. **CPU 的 `SYSCALL/SYSRET` 指令语义**：硬件在特权级切换时实际改了哪些寄存器。

本文把三层分开，并以当前 Linux x86-64 入口代码为基准。内核实现会演进；复制源码片段时应注明版本，而不是把某个版本的细节当成永久 ABI。

## 1. Linux syscall 寄存器约定

| 用途 | 寄存器 |
|---|---|
| 系统调用号 | `rax` |
| 参数 1–3 | `rdi`, `rsi`, `rdx` |
| 参数 4–6 | `r10`, `r8`, `r9` |
| 返回值 | `rax` |
| 硬件破坏 | `rcx`, `r11` |

典型调用：

```asm
mov $1, %rax          # __NR_write
mov $1, %rdi          # fd = STDOUT_FILENO
lea message(%rip), %rsi
mov $6, %rdx
syscall
```

Linux 内核直接返回非负结果或 `-errno`。glibc 等 libc wrapper 再把 `-errno` 转换为 `-1`，并设置线程局部的 `errno`。

### 为什么第 4 个参数是 r10，而不是 rcx

普通 System V 函数的第 4 个整数参数使用 `rcx`；但 `SYSCALL` 指令必须把用户态返回地址写入 `rcx`。因此 Linux syscall ABI 把第 4 个参数移到 `r10`。

这也是 inline asm 必须声明 `rcx` 和 `r11` clobber 的原因：

```c
static inline long raw_write(int fd, const void *buf, unsigned long count)
{
    register long rax __asm__("rax") = 1;   /* __NR_write */
    register long rdi __asm__("rdi") = fd;
    register long rsi __asm__("rsi") = (long)buf;
    register long rdx __asm__("rdx") = count;

    __asm__ volatile (
        "syscall"
        : "+a"(rax)
        : "D"(rdi), "S"(rsi), "d"(rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
}
```

对于 4–6 个参数，需要显式绑定 `r10`、`r8`、`r9`。生产代码通常应优先调用 libc；raw syscall 会绕过取消点、兼容封装和 libc 的错误处理。

## 2. `SYSCALL` 指令做什么

在 64 位模式下执行 `SYSCALL` 时，CPU 主要完成：

```text
RCX    <- 下一条用户态指令地址
R11    <- 用户态 RFLAGS
RIP    <- IA32_LSTAR
RFLAGS <- RFLAGS & ~IA32_FMASK
CS/SS  <- 由 IA32_STAR 派生的内核选择子
CPL    <- 0
```

两个关键结论：

- `rcx` 和 `r11` 被硬件占用，不能用于保存跨 syscall 的值。
- `SYSCALL` **不会保存用户态 `rsp`，也不会自动切到内核栈**。

RFLAGS 并非简单“保持不变”。内核在 `IA32_FMASK` 中配置要清除的标志位；Linux 的入口代码在建立安全状态后再管理中断。

## 3. Linux `entry_SYSCALL_64`

当前 x86-64 入口的核心顺序是：

```asm
# arch/x86/entry/entry_64.S（省略安全加固与注解）
entry_SYSCALL_64:
    swapgs
    movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)  # 暂存用户 rsp
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
    movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    pushq $__USER_DS
    pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    pushq %r11
    pushq $__USER_CS
    pushq %rcx
    pushq %rax
    # 继续构造 struct pt_regs，随后调用 do_syscall_64
```

这里的 `tss.sp2` 是 Linux 使用的 per-CPU scratch slot，不是传统意义上由硬件自动切栈的 `sp0` 流程。顺序也很重要：先保存用户 `rsp`，再切内核页表与内核栈，最后在内核栈上构造 `struct pt_regs`。

因此，用户态 red zone 不会被普通 syscall 入口上的寄存器帧覆盖；帧位于内核栈，而不是“用户 `rsp` 下方”。

## 4. 返回：SYSRET 快路径与 IRET 回退

系统调用结束后，Linux 会检查返回上下文是否适合 `SYSRET`。只有在地址、段寄存器和标志位等条件都安全时才走快路径；否则使用 `IRET`。

这样做不是单纯的性能选择。AMD 和 Intel 的 `SYSRET` 对非 canonical 地址等边界情况存在难处理的行为，内核必须先验证用户可控的 `pt_regs`。

从用户态观察，两条返回路径都应满足同一 syscall ABI；不能依赖内核一定使用 `SYSRET`。

## 5. Red zone 与系统调用

System V AMD64 ABI 在用户 `rsp` 下方定义 128 字节 red zone。叶子函数可以临时使用这块空间而不调整 `rsp`。

需要区分三种情况：

- **普通 syscall/中断进入内核**：Linux 切换到内核栈，入口帧不写用户 red zone。
- **向用户态递送 signal**：内核构造 signal frame 时会为 red zone 留出空间。
- **内核代码本身**：内核以 `-mno-red-zone` 编译，不能依赖用户态 ABI 的 red zone。

手写 inline asm 时，`"memory"` clobber 只约束编译器的内存重排；它不代表 syscall 只会访问某个固定内存范围。传给内核的指针指向哪些缓冲区，仍由具体 syscall 定义。

## 6. vDSO 不是“在用户态执行 syscall”

vDSO 是内核映射进进程地址空间的一小段 ELF 代码。某些 libc API 可以先调用 vDSO，从内核维护的只读数据中计算结果，从而避免真正执行 `SYSCALL`。

常见符号包括：

- `__vdso_clock_gettime`
- `__vdso_gettimeofday`
- `__vdso_time`
- `__vdso_getcpu`
- 新内核上的 `__vdso_getrandom`

这些符号是否存在、能否完成请求取决于架构和内核版本；libc 必须准备回退到真正的 syscall。

检查当前进程的 vDSO：

```bash
grep '\[vdso\]' /proc/self/maps

# glibc 环境可观察实际调用是否仍进入内核
strace -e trace=clock_gettime,gettimeofday /bin/date
```

## 7. `int 0x80`、`sysenter` 与 64 位 `syscall`

三者属于不同入口机制，不能只用一张固定“周期数”表比较：

| 机制 | 主要使用场景 | 入口配置 |
|---|---|---|
| `int 0x80` | 传统 i386 ABI；x86-64 下也有兼容入口 | IDT |
| `sysenter/sysexit` | 32 位快速系统调用 | SYSENTER MSR |
| `syscall/sysret` | 原生 x86-64 Linux syscall ABI | STAR/LSTAR/FMASK MSR |

实际延迟受 CPU 微架构、KPTI、缓解措施、虚拟化、频率和内核版本影响。需要性能数据时，应在目标机器上测量，并报告环境：

```bash
perf stat -r 20 ./syscall-benchmark
```

不要把某台旧 CPU 上的 `50–70 cycles` 当成跨平台事实。

## 8. 完整汇编示例

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

.section .text
.global _start
_start:
    mov $1, %rax          # __NR_write
    mov $1, %rdi          # stdout
    lea message(%rip), %rsi
    mov $6, %rdx
    syscall

    test %rax, %rax
    js .error

    xor %rdi, %rdi
    jmp .exit

.error:
    mov $1, %rdi
.exit:
    mov $60, %rax         # __NR_exit
    syscall
```

构建与观察：

```bash
as -o syscall.o syscall.S
ld -o syscall syscall.o
strace ./syscall
objdump -drwC -Mintel ./syscall
```

## 9. 权威资料

- [Linux `entry_SYSCALL_64` 当前源码](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/entry_64.S)
- [Linux x86 syscall 参数映射](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/entry/syscalls/syscall_64.tbl)
- [Linux man-pages `syscall(2)`](https://man7.org/linux/man-pages/man2/syscall.2.html)
- [x86-64 psABI](https://gitlab.com/x86-psABIs/x86-64-ABI)
- [Intel 64 and IA-32 Architectures Software Developer Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html)

阅读顺序建议：先用 `syscall(2)` 确认 Linux ABI，再用处理器手册确认 `SYSCALL/SYSRET` 指令语义，最后对照目标内核版本的 `entry_64.S`。这样不会把 CPU 规则、用户态 C ABI 和某一版内核实现混为一谈。
