本页目录

x86-64 系统调用 ABI

覆盖: syscall 指令 → 参数寄存器约定 → red zone → vDSO → vsyscall → 与 int 0x80 / sysenter 的历史对比 适用: x86-64 (AMD64), Linux 3.x+

概述

x86-64 系统调用使用 syscall 指令(Intel 语法: syscall),它是 AMD64 引入的快速系统调用机制,替代了 32-bit x86 的三代演进(int 0x80sysenter/sysexitsyscall/sysret)。理解调用约定对于手写汇编、inline asm、或只是读反汇编时的 syscall 指令都很有用。

寄存器约定

# 调用号 (syscall number):
  rax = syscall number ( __NR_read = 0, __NR_write = 1, ...)

# 参数 (最多 6 个):
  rdi = arg1    rsi = arg2    rdx = arg3
  r10 = arg4    r8  = arg5    r9  = arg6

# 返回值:
  rax = return value (>=0: success, -errno: error → 用户态 libc 转换为 errno)
  rdx = 第二个返回值 (仅用于少数 syscall)

# Clobbered (被 syscall 破坏):
  rcx = 保存的 RIP (syscall 指令写 RCX ← RIP)
  r11 = 保存的 RFLAGS (syscall 指令写 R11 ← RFLAGS)

# Preserved (syscall 不会改):
  rbx, r12-r15, rbp, rsp

注意 r10 替代 rcx 作为第 4 个参数——因为 syscall 指令自己用 rcx 保存返回地址。

syscall 指令行为

syscall:
  1. RCX ← RIP   (保存用户态返回地址)
  2. R11 ← RFLAGS
  3. RIP ← IA32_LSTAR (MSR 0xC0000082, 指向 entry_SYSCALL_64)
  4. CS  ← IA32_STAR[47:32]
  5. SS  ← IA32_STAR[47:32] + 8
  6. 切换到 Ring 0 (内核态)
  → 内核 now running with kernel stack, IF unchanged

sysretq (返回):
  1. RIP ← RCX   (恢复用户态下一条指令)
  2. RFLAGS ← R11 (恢复标志寄存器, 低 32 位)
  3. 切换到 Ring 3 (用户态)

关键: syscall 不自动切换栈——内核必须在 entry_SYSCALL_64 中通过 swapgs + 读 per-CPU kernel_stack 手动切换到内核栈。

内核入口: entry_SYSCALL_64

# arch/x86/entry/entry_64.S
entry_SYSCALL_64:
    swapgs                    # GS 基址从用户态切换到内核态 per-CPU 区域
    movq  %rsp, PER_CPU(cpu_tss_rw + TSS_sp0)  # 保存用户栈到 TSS
    movq  PER_CPU(pcpu_hot + X_top_of_stack), %rsp  # 切到内核栈

    pushq $__USER_DS          # 构造 iret frame (SS)
    pushq PER_CPU(cpu_tss_rw + TSS_sp0)  # (RSP)
    pushq %r11                # (RFLAGS)
    pushq $__USER_CS          # (CS)
    pushq %rcx                # (RIP)

    # 调用 do_syscall_64(rdi=pt_regs, rax=nr)
    call do_syscall_64

Red Zone: x86-64 ABI 的陷阱

x86-64 ABI: 栈指针 (RSP) 以下 128 bytes 是 "red zone"
   → 信号处理程序不能碰这 128 bytes
   → 编译器可以在不调整 RSP 的情况下用这块区域 (leaf functions)

与内核的交互:
  进入内核时: 内核在 RSP 以下放 iret frame → 它不会碰 red zone
  返回用户态: 用户态可能依赖 red zone 的数据 → 内核不能破坏

  信号处理: 安装 sigframe 时必须考虑 red zone
    → 信号栈帧从 RSP-128 以下开始

vDSO: 不进入内核的系统调用

// arch/x86/entry/vdso/
// 部分系统调用通过 vDSO 直接在用户态执行:

// __vdso_clock_gettime(): 读 TSC + 应用 timekeeper 修正 → 不 syscall!
// __vdso_getcpu():        读 per-CPU 变量 → 不 syscall!
// __vdso_time():          读 timekeeper 缓存 → 不 syscall!
// __vdso_getrandom():     读内核维护的随机数池 → 不 syscall!

// vsyscall (旧, 已废弃):
//   固定在 0xffffffffff600000 → 安全风险 (固定地址 = ROP 目标)
//   现在默认 emulate (通过 page fault 模拟) → 极慢 → 应该用 vDSO

与 32-bit x86 的对比

                         int 0x80        sysenter         syscall (x86-64)
架构                      x86             x86 (Pentium II+)  x86-64 (AMD64)
保存返回地址              无 (软件)       硬件: ECX ← EIP   硬件: RCX ← RIP
CPU 上下文切换            通过 IDT         通过 MSR           通过 MSR
返回值                     eax             eax               rax
clobbered 寄存器          少               ecx, r11           rcx, r11
延迟 (cycles)             ~150             ~80               ~50-70

完整系统调用示例

# 手写 write(1, "hello\n", 6) — x86-64 Linux
    movq $1, %rax        # __NR_write = 1
    movq $1, %rdi        # fd = stdout
    leaq msg(%rip), %rsi # buf
    movq $6, %rdx        # count
    syscall

    movq $60, %rax       # __NR_exit = 60
    xorq %rdi, %rdi      # exit code = 0
    syscall

msg: .ascii "hello\n"

参考

  • 源码⁠: arch/x86/entry/entry_64.S, arch/x86/entry/vdso/, arch/x86/include/asm/syscall.h
  • ABI 文档⁠: System V AMD64 ABI, Linux x86-64 syscall table: /usr/include/asm/unistd_64.h
  • LWN: "The vDSO and vsyscall", "Faster syscalls"

关键词: syscall, sysret, entry_SYSCALL_64, red zone, vDSO, vsyscall, swapgs, calling convention