系统调用(System Calls)

Linux 下通过 syscall 指令(x86-64)或 int $0x80(32 位)陷入内核。系统调用号放入 %rax,参数依次放入 %rdi %rsi %rdx %r10 %r8 %r9,返回值在 %rax

重要:第 4 个参数在 syscall ABI 中使用 %r10 而非 %rcx——因为 syscall 指令会把返回地址存入 %rcx、把 RFLAGS 存入 %r11,这两个寄存器总是被破坏。除此之外,内核保留其余寄存器(除 %rax 返回值外)。

64 位:syscall

常用调用号(x86-64 /usr/include/asm/unistd_64.h):

调用 号(%rax) 参数
read 0 fd(rdi), buf(rsi), count(rdx)
write 1 fd(rdi), buf(rsi), count(rdx)
open 2 path(rdi), flags(rsi), mode(rdx)
close 3 fd(rdi)
lseek 8 fd(rdi), offset(rsi), whence(rdx)
mmap 9 addr(rdi), len(rsi), prot(rdx), flags(r10), fd(r8), off(r9)
ioctl 16 fd(rdi), req(rsi), arg(rdx)
getpid 39
exit 60 code(rdi)
exit_group 231 code(rdi)(终止整个进程组,libc 的 exit 使用)
# write(1, msg, len)
movq $1, %rax          # syscall number: write
movq $1, %rdi          # fd = 1 (stdout)
leaq msg(%rip), %rsi   # buf(PIC 推荐写法;非 PIE 也可 movq $msg, %rsi 绝对地址)
movq $13, %rdx         # count
syscall

# exit(0)
movq $60, %rax
xorq %rdi, %rdi
syscall

32 位:int $0x80

调用号放 %eax,参数 %ebx %ecx %edx %esi %edi %ebp

movl $4, %eax          # write
movl $1, %ebx          # fd
movl $msg, %ecx        # buf
movl $13, %edx         # count
int  $0x80

movl $1, %eax          # exit
xorl %ebx, %ebx
int  $0x80

现代 32 位内核优先走 sysenter 快速路径(libc 内部处理),int $0x80 是兼容旧路径,可直接使用但较慢。

vDSO:不陷入内核的"系统调用"

gettimeofdayclock_gettimegetcpu 等高频调用由内核把代码映射到每个进程的 vDSO 页,用户态直接执行,不真正陷入内核。用 strace 观察程序时看不到这些调用,属正常现象。

完整最小程序(64 位,直接系统调用,无需 libc)

.section .rodata
msg:    .ascii "Hello, World!\n"
.set len, . - msg

.section .text
.globl _start
_start:
    movq $1, %rax
    movq $1, %rdi
    leaq msg(%rip), %rsi      # RIP 相对取地址(PIC)
    movq $len, %rdx
    syscall

    movq $60, %rax
    xorq %rdi, %rdi
    syscall

ld 默认入口符号是 _start;用 ld -e my_entry 可改。经 gcc -nostdlib 链接同样如此。

错误处理

系统调用失败时 %rax 为负的 errno(如 -14 表示 EFAULT)。用户态需取绝对值得到 errno:

    cmpq $0, %rax
    jge   ok              # rax >= 0 成功
    negq  %rax            # 失败:rax = -errno -> errno
ok:

另外注意:shell 里 $? 只能看到退出码的低 8 位(0–255),用退出码传递大数值不可靠。