跳转到内容

系统调用 ABI

OpenOS 使用 x86_64 的 syscall 指令实现用户态到内核态的快速切换。这是 x86_64 上最高效的用户→内核转换方式。

寄存器syscall 入口时sysretq 返回时
RAX系统调用号返回值
RDI参数 1保留
RSI参数 2保留
RDX参数 3保留
R10参数 4(保留)保留
RCX被 CPU 覆盖(用户 RIP)被 CPU 覆盖
R11被 CPU 覆盖(用户 RFLAGS)被 CPU 覆盖

执行 syscall 指令时,CPU 自动执行:

  1. 保存返回地址 - RIP → RCX
  2. 保存标志寄存器 - RFLAGS → R11
  3. 加载内核段 - 从 STAR MSR 加载 CS/SS
  4. 禁用中断 - 清除 RFLAGS 中的 IF

执行 sysretq 指令时,CPU 自动执行:

  1. 恢复返回地址 - RCX → RIP
  2. 恢复标志寄存器 - R11 → RFLAGS
  3. 加载用户段 - 从 STAR MSR 加载 CS/SS
RAX = 结果值 (正数)
RAX = 错误码 | (1 << 63) (高位为 1)

错误检测:

; 检查是否发生错误
test rax, rax
js error_handler ; 如果符号位为 1,跳转到错误处理

错误码提取:

; 提取错误码(去除高位标志)
and rax, 0x7FFFFFFFFFFFFFFF
编号名称说明
0(保留)
1WRITE写入控制台
2READ读取输入
3EXIT终止进程
4YIELD让出 CPU
5PORT_CREATE创建端口
6SEND发送消息
7RECEIVE接收消息
; OpenOS 系统调用包装器
; 输入:rax = 系统调用号, rdi = 参数1, rsi = 参数2, rdx = 参数3
; 输出:rax = 结果(检查第 63 位判断是否错误)
section .text
openos_syscall:
mov r10, rcx ; 保存参数 4(如果需要)
syscall
ret
; 示例:SYS_WRITE
; 输入:rdi = 缓冲区指针, rsi = 长度
openos_write:
mov rax, 1 ; SYS_WRITE
syscall
ret
; 示例:SYS_READ
; 输入:rdi = 缓冲区指针, rsi = 最大长度
openos_read:
mov rax, 2 ; SYS_READ
syscall
ret
; 示例:SYS_EXIT
; 输入:rdi = 退出码
openos_exit:
mov rax, 3 ; SYS_EXIT
syscall
; 不会返回
; user/hello.asm
; OpenOS 用户态示例程序
section .data
msg: db "Hello from user-space!", 10
msg_len equ $ - msg
section .text
global _start
_start:
; 写入消息
mov rax, 1 ; SYS_WRITE
lea rdi, [rel msg] ; 缓冲区指针
mov rsi, msg_len ; 长度
syscall
; 检查错误
test rax, rax
js .error
; 退出
mov rax, 3 ; SYS_EXIT
xor rdi, rdi ; 退出码 0
syscall
.error:
; 错误处理
mov rax, 3 ; SYS_EXIT
mov rdi, 1 ; 退出码 1
syscall
参数寄存器大小
系统调用号RAXu64
参数 1RDIu64
参数 2RSIu64
参数 3RDXu64
参数 4R10u64(保留)
返回值RAXu64
位 0-31: SYSCALL 目标地址(RIP)
位 32-47: SYSCALL CS/SS(内核段选择子)
位 48-63: SYSRET CS/SS(用户段选择子)
SYSCALL 入口点地址(RIP)
SYSCALL 时清除的 RFLAGS 位掩码

为了支持 SYSCALL/SYSRET,GDT 必须按特定顺序排列:

Index 0: 空描述符 (0x00)
Index 1: 内核代码 (0x08, Ring 0)
Index 2: 内核数据 (0x10, Ring 0)
Index 3: 用户数据 (0x18, Ring 3) ← 必须在用户代码之前
Index 4: 用户代码 (0x20, Ring 3)
Index 5-6: TSS

注意: 用户数据必须在用户代码之前,这是 SYSRET 的要求。

执行 syscall 时,CPU 自动禁用中断(清除 IF 位)。这意味着:

  1. 系统调用是原子的 - 不会被中断打断
  2. 需要手动启用中断 - 如果系统调用处理函数需要中断
  3. 避免死锁 - 不需要在系统调用开始时禁用中断
  • 寄存器传递 - 所有参数通过寄存器传递,无内存访问
  • 无内存屏障 - syscall/sysret 不需要内存屏障
  • 中断禁用 - 避免在系统调用期间处理中断
  • 参数验证 - 需要验证用户态指针
  • 内存复制 - 消息传递需要复制数据
  • 上下文切换 - 如果需要调度
/// 验证用户态指针
fn validate_user_ptr(ptr: *const u8, len: usize) -> bool {
// 检查指针是否在用户空间
if ptr as usize >= 0x800000000000 {
return false;
}
// 检查长度是否合理
if len > MAX_MESSAGE_SIZE {
return false;
}
// 检查指针是否对齐
if ptr as usize % core::mem::align_of::<u8>() != 0 {
return false;
}
true
}
/// 检查系统调用权限
fn check_syscall_permission(number: u64) -> bool {
// 某些系统调用需要特殊权限
match number {
SYS_WRITE | SYS_READ | SYS_EXIT | SYS_YIELD => true,
SYS_PORT_CREATE | SYS_SEND | SYS_RECEIVE => true,
_ => false,
}
}

使用 GDB 跟踪系统调用:

Terminal window
# 启动 QEMU
make debug
# 在 GDB 中
(gdb) break syscall::handle_syscall
(gdb) continue
# 查看系统调用参数
(gdb) print number
(gdb) print arg1
(gdb) print arg2
(gdb) print arg3

问题:系统调用返回 -1

  • 可能原因:无效的系统调用号
  • 解决:检查 RAX 中的系统调用号

问题:系统调用卡住

  • 可能原因:中断未正确处理
  • 解决:检查 PIC 初始化和 EOI 发送