3-系统调用和陷入
System calls and Trap
- 用户态和内核态的切换(trap)
- 程序执行系统调用(System call)
- 程序出现了类似 page fault、除 0 等异常(软件中断)
- 硬件中断(IO 设备)
- RISC-V 寄存器
- 32 个通用寄存器
- PC(程序计数器)
- Mode(表明位于何种状态 user or supervisor)
- SATP(指向页表的物理地址)
- SEPC(指向 trap 指令的起始地址)
- STVEC (也就是处理 trap 的内核指令地址)
- SSRATCH(交换页表和 a0 地址)
- 内核态权限
- 读写控制寄存器:SATP、STVEC、SEPC
- 它可以使用 PTE_U 标志位为 0 的 PTE
- supervisor mode 中的代码并不能读写任意物理地址,通过 page table 访问内存
- 用户态跳入内核态前
- 保存用户态 32 个寄存器
- 保存 PC
- 改变 Mode 为 Supervisor
- 改变 SATP 寄存器指向内核页表
- 将堆栈寄存器指向内核的地址
Trap 执行流程
在从用户空间进入到内核空间之前,内核会设置好 STVEC 寄存器指向 trampoline page
ecall(CPU 指令)切换到 Supervisor mode
- 此时并未切换 page table,仍然是用户页表,代码正在 trampoline page 的最开始
- 保存 PC 到 SEPC 寄存器
- ecall 会跳转到 STVEC 寄存器指向的指令
RISC-V 秉持了这样一个观点:ecall 只完成尽量少必须要完成的工作,其他的工作都交给软件完成。这里的原因是,RISC-V 设计者想要为软件和操作系统的程序员提供最大的灵活性,这样他们就能按照他们想要的方式开发操作系统
uservec:汇编语言代码,trampoline.s 文件的一部分
内核会将 trapframe page 的地址保存 SSCRATCH 寄存器
用户映射了 trapframe page,每个进程有自己的 trapframe page;用该结构保存 32 个用户寄存器
在内核前一次切换回用户空间时,内核会执行 set sscratch 指令,将这个寄存器的内容设置为 0x3fffffe000,也就是 trapframe page 的虚拟地址
在返回用户空间前,设置 a0 为 trapframe page 的虚拟地址,同时切换页表为用户页表
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void(*)(uint64,uint64))fn)(TRAPFRAME, satp);切换到内核页表;因为二者映射的物理地址相同,切换后可以正常运行而不崩溃
trampoline page 在 user page table 中的映射与 kernel page table 中的映射是完全一样的。这两个 page table 中其他所有的映射都是不同的,只有 trampoline page 的映射是一样的,因此我们在切换 page table 时,寻址的结果不会改变,我们实际上就可以继续在同一个代码序列中执行程序而不崩溃
跳入 usertrap 函数
usertrap
先将 STVEC 指向了 kernelvec 变量,这是内核空间 trap 处理代码的位置
我们需要知道当前运行的是什么进程,我们通过调用 myproc 函数来做到这一点。myproc 函数实际上会查找一个根据当前 CPU 核的编号索引的数组,CPU 核的编号是 hartid,如果你还记得,我们之前在 uservec 函数中将它存在了 tp 寄存器。这是 myproc 函数找出当前运行进程的方法。
当程序还在内核中执行时,我们可能切换到另一个进程,并进入到那个程序的用户空间,然后那个进程可能再调用一个系统调用进而导致 SEPC 寄存器的内容被覆盖。所以,我们需要保存当前进程的 SEPC 寄存器到一个与该进程关联的内存中
p->trapframe-epc = r_sepc();
syscall
- 根据系统调用的编号查找相应的系统调用函数。Shell 调用的 write 函数将 a7 设置成了系统调用编号 16
- 向 trapframe 中的 a0 赋值,所有的系统调用都有一个返回值
usertrapret
- 关闭中断
- 设置了 STVEC 寄存器指向 trampoline 代码,在那里最终会执行 sret 指令返回到用户空间
- 设置 trapframe 参数供下一次 trap 使用
- 存储了 kernel page table 的指针
- 存储了当前用户进程的 kernel stack
- 存储了 usertrap 函数的指针,这样 trampoline 代码才能跳转到这个函数
- 从 tp 寄存器中读取当前的 CPU 核编号,并存储在 trapframe 中,这样 trampoline 代码才能恢复这个数字,因为用户代码可能会修改这个数字
- 设置 SSTATUS 寄存器
- SEPC 寄存器的值设置成之前保存的用户程序计数器的值
- 根据 user page table 地址生成相应的 SATP 值
- trapframe 地址作为第一个参数;将 page table 指针准备好,并将这个指针作为第二个参数传递给汇编代码,这个参数会出现在 a1 寄存器;fn 为函数跳转位置
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void(*)(uint64,uint64))fn)(TRAPFRAME, satp);userret,位于 trampoline.s 文件
切换为用户页表
之前保存的寄存器的值加载到对应的各个寄存器中
交换 SSCRATCH 寄存器和 a0 寄存器的值,保证 SSCRATCH 持有的是 trapframe 的地址
sret
- 程序切换为 user mode
- SEPC 的值拷贝到 PC
- 开中断
- 硬件和软件需要协同工作,你可能需要重新设计 XV6,重新设计 RISC-V 来使得这里的处理流程更加简单,更加快速。
- 另一个需要时刻记住的问题是,恶意软件是否能滥用这里的机制来打破隔离性。