本页目录
Linux x86-64 系统调用 ABI
写手工汇编、inline asm、调试 seccomp 或阅读反汇编时,最容易混淆的是三套不同规则:
- System V AMD64 C ABI:普通用户态函数怎样传参、返回和保存寄存器。
- Linux x86-64 syscall ABI:用户态怎样把系统调用号和最多 6 个参数交给内核。
- CPU 的
SYSCALL/SYSRET指令语义:硬件在特权级切换时实际改了哪些寄存器。
本文把三层分开,并以当前 Linux x86-64 入口代码为基准。内核实现会演进;复制源码片段时应注明版本,而不是把某个版本的细节当成永久 ABI。
1. Linux syscall 寄存器约定
| 用途 | 寄存器 |
|---|---|
| 系统调用号 | rax |
| 参数 1–3 | rdi, rsi, rdx |
| 参数 4–6 | r10, r8, r9 |
| 返回值 | rax |
| 硬件破坏 | rcx, r11 |
典型调用:
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 的原因:
static inline long
对于 4–6 个参数,需要显式绑定 r10、r8、r9。生产代码通常应优先调用 libc;raw syscall 会绕过取消点、兼容封装和 libc 的错误处理。
2. SYSCALL 指令做什么
在 64 位模式下执行 SYSCALL 时,CPU 主要完成:
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 入口的核心顺序是:
# 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:
# glibc 环境可观察实际调用是否仍进入内核
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、缓解措施、虚拟化、频率和内核版本影响。需要性能数据时,应在目标机器上测量,并报告环境:
不要把某台旧 CPU 上的 50–70 cycles 当成跨平台事实。
8. 完整汇编示例
.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
构建与观察:
9. 权威资料
- Linux
entry_SYSCALL_64当前源码 - Linux x86 syscall 参数映射
- Linux man-pages
syscall(2) - x86-64 psABI
- Intel 64 and IA-32 Architectures Software Developer Manuals
阅读顺序建议:先用 syscall(2) 确认 Linux ABI,再用处理器手册确认 SYSCALL/SYSRET 指令语义,最后对照目标内核版本的 entry_64.S。这样不会把 CPU 规则、用户态 C ABI 和某一版内核实现混为一谈。