之前所有代码都跑在内核态(ring0)。这一章第一次让用户程序在 ring3 里运行——加载 ELF 文件,跳到用户态执行,用户程序通过 syscall 和内核通信。
这是操作系统最核心的一个边界:内核和用户程序之间的隔离。
exec 做什么
exec 的语义是"用一个新程序替换当前进程":
- 从文件系统读 ELF 文件
- 解析 ELF header,找到各个段的加载地址
- 创建新的用户页表,把各段加载进去
- 分配用户栈
- 设置进程的 rip = 入口地址,rsp = 栈顶,cs/ss = 用户段,放入就绪队列
调度器下次选中这个进程,就会跳到用户态开始执行。
进入用户态:iretq
第一次进入用户态不能用 sysret——没有对应的 syscall。用 iretq:
static void enter_usermode(uint64_t rip, uint64_t cs,
uint64_t rflags, uint64_t rsp, uint64_t ss) {
__asm__ volatile(
"push %4\n" // ss
"push %3\n" // rsp
"push %2\n" // rflags(IF=1,开中断)
"push %1\n" // cs(0x23,ring3 代码段)
"push %0\n" // rip(entry point)
"iretq"
:: "r"(rip), "r"(cs), "r"(rflags), "r"(rsp), "r"(ss)
);
}
iretq 弹出这 5 个字段,CPU 检查 cs 的 RPL 发现是 ring3,自动完成特权级切换。
用户态 → 内核:syscall/sysret
用户程序通过 syscall 指令进内核,sysret 返回用户态。
syscall 执行时:跳到 LSTAR 指定的内核入口,RCX 保存用户返回地址,R11 保存用户 rflags,但不切换栈——RSP 还是用户栈。
所以内核入口必须用汇编手动保存用户寄存器,C 函数处理完之后再恢复,最后 o64 sysret 回用户态。
初始化时写三个 MSR:
wrmsr(0xC0000080, efer | 1); // EFER.SCE:启用 syscall/sysret
wrmsr(0xC0000082, (uint64_t)syscall_entry); // LSTAR:内核入口地址
wrmsr(0xC0000081, ((uint64_t)0x001B0008 << 32)); // STAR:CS/SS 选择子
wrmsr(0xC0000084, 0x200); // SFMASK:syscall 时清 IF
用户程序
用户程序用汇编直接发 syscall,不依赖任何 C 运行时:
_start:
mov rax, 1 ; syscall 1 = write
lea rdi, [rel msg] ; 字符串地址
syscall
mov rax, 2 ; syscall 2 = exit
syscall
msg: db "Hello from user!", 13, 10, 0
用 -nostdlib 编译,链接脚本把 _start 放在 ELF 入口点,写到 ext2 镜像里,内核从磁盘加载运行。
验证
ext2 mounted!
[test] exec hello.elf
[test] exec ok, pid=0x1
Hello from user!
[kernel] process exited
用户程序在 ring3 运行,write syscall 打印字符串,exit syscall 退出,没有 Page Fault。
踩坑:NASM 里 sysretq 是标签不是指令
这一章最隐蔽的 bug。
写 sysretq,NASM 不报错,只输出一条 warning:
warning: label alone on a line without a colon
NASM 没有 sysretq 这个助记符,把它当成了标签定义——代码直接跌落到下一个函数 gdt_init,CPU 跑飞,Page Fault。
debug:看到 Page Fault 的 RIP 是 0x10194,反汇编发现是 gdt_init.reload_cs,才意识到 sysretq 根本没生成任何指令。
修法:改为 o64 sysret。
另一个坑:syscall_entry 里绝对不能 call C 函数做调试输出。C 函数会按 ABI 破坏 caller-saved 寄存器,包括 RCX——sysret 需要它作为用户返回地址,一旦被覆盖就跳到错误地址,直接 Page Fault。
小结
| 机制 | 要点 |
|---|---|
iretq |
首次进用户态,手动构造 5 字段栈帧 |
syscall |
不切栈,RCX = 返回地址,R11 = rflags |
o64 sysret |
回用户态,NASM 里不能写 sysretq |
下一章:wait / exit — 父进程等待子进程退出,拿到退出码。