本页目录
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 0x80 → sysenter/sysexit → syscall/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