Skip to content

6 - Isolation & System Call Entry_Exit

Trap

硬件:
在 Risc-V 的 CPU 中会有 32 寄存器(registers),其中一些是特殊的寄存器:

栈指针 x2 sp (stack pointer)
程序计数器 pc (program counter)

satp (satp) ,指向页表的指针,用于页表的查找
stvec (Supervisor Trap Vector Base Address) ,指向中断向量表的指针,用于处理 kernel 中的 trap
sepc (sepc) ,保存触发 trap 的指令地址,指向当前正在执行的指令的地址,用于保存系统调用的入口和出口,pc
sscratch (sscratch) ,用于保存 user 态和 kernel 态之间的切换状态


当 mode 是 user 模式的时候,拥有的权限是可以读写少数 control 寄存器,只能使用 PTE_U 的 PTE ,如果在 super 模式时,所有寄存器都可以读写,但是默认不使用 PTE_U 的 PTE。

在 trap 发生的之前, CPU 的所有状态都设置为运行 user 代码,而不是 kernel 代码。操作系统需要实现几点:
需要透明的恢复 user 代码:特别是在 user 预期设备不会中断的情况下,希望让 kernel 实现中断,然后恢复 user 代码,而 user 感觉不到这个过程,因此不能让 kernel 干扰这个过程,也需要将原本的 kernel 的寄存器信息保存起来;
需要实现安全和隔离: user 的代码不能干扰 user kernel 的转换,也不能依赖任何 user 态传递过来的东西,它们可能有恶意。 xv6 中,甚至不会去查看寄存器的值,只是保存起来。


在系统当中,有 user 态和 kernel 态, user 态有 user 程序,,例如 shell ,当 shell 希望进行系统调用时,例如 write 。
首先会将模式切换到 super 模式,因为需要使用到 kernel 的权限, satp 目前之前 user 页表,它只包含 user 程序需要的映射,但是 user 的页表没有 kernel 页表的映射,因此需要切换页表到 kernel 页表,栈指针需要指向 kernel 中某个位置的栈等等,切换完所有的寄存器状态。

当 shell 调用 write 时,write 会调用 ecall 指令,直接跳到 super mode

之后的第一条指令是用 assembled 编写的 uservec() ,这是 trampoline 的一部分,它在 trampoline.S 中实现。
接着执行 trap.c 中的 usertrap()
usertrap() 会调用 syscall()syscall() 会查看系统调用号 sys_write ,然后执行对应的系统调用
当系统调用执行结束,返回时
sys_write() 会返回到 syscall()
syscall() 会返回到 usertrap()
然后 usertrap() 会调用usertrapret()
最后会返回到 trampoline.S 中的 userret()

对于寄存器:

  1. ecall 前(用户态)
    • 寄存器全是用户值:a7=系统调用号,a0..=参数,sp/ra… 也是用户的。
  2. 执行 ecall(硬件行为)
    • 切到 S 态;sepc ← ecall 的地址;scause ← U-ecall;pc ← stvec;GPR 原封不动(仍是用户值)。
  3. 入口汇编 trampoline.S:uservec
    • 从 sscratch 取到当前进程的 trapframe 指针。
    • 把所有用户 GPR(ra、sp、a0..a7、…)保存到 trapframe。
      注:这一步“顺带”保存了系统调用参数和号,因为它们就在 a0..a7 里。
    • 切到内核页表(写 satp + sfence.vma)、切到该进程的内核栈。
    • 跳到 C 函数 trap.c:usertrap。
  4. trap.c:usertrap(内核 C 入口)
    • p->trapframe->epc = r_sepc()(把硬件 CSR 里的 sepc 拷到 trapframe,用作返回的 pc)。
    • 若是系统调用:p->trapframe->epc += 4(返回时跳过 ecall);然后调用 syscall()。
  5. syscall() 分发与执行
    • 从 p->trapframe->a7 取系统调用号;从 p->trapframe->a0.. 取参数。
    • 对指针参数用 copyin/copyout 校验并拷贝,避免直接信任用户地址。
    • 调用具体 sys_* 实现;把返回值写回 p->trapframe->a0。
  6. 返回用户态准备 usertrapret → trampoline.S:userret
    • 把 stvec 设回 uservec(为下一次 U→S 做好入口)。
    • userret:设置 sstatus.SPP=U;w_sepc(trapframe->epc);切回用户页表;从 trapframe 逐一恢复所有 GPR(此时 a0 已是返回值);sret 回到用户态,执行 ecall 之后的一条指令。
mermaid
sequenceDiagram
    participant User as 用户态 (User)
    participant HW as 硬件 (CSR)
    participant ASM as 汇编 (trampoline.S)
    participant Kernel as 内核 C (trap.c)

    User->>HW: 执行 ecall 指令
    HW->>HW: U 态→S 态,保存 PC 到 sepc
    HW->>ASM: 跳转到 stvec (uservec)
    ASM->>ASM: 交换 sp & sscratch (保存用户栈)
    ASM->>ASM: 保存所有寄存器到 trapframe
    ASM->>ASM: 切换 satp (内核页表) & sp (内核栈)
    ASM->>Kernel: 调用 usertrap()
    Kernel->>Kernel: 识别系统调用,epc += 4
    Kernel->>Kernel: 执行 syscall() (可能 yield)
    Kernel->>ASM: 返回 usertrapret() -> userret()
    ASM->>ASM: 恢复寄存器,切换回用户页表
    ASM->>HW: 执行 sret
    HW->>User: S 态→U 态,恢复 PC (sepc)

系统调用全流程(精简):

  1. 用户程序调用 read()
  2. 执行 ecall 指令 → Trap 发生
  3. CPU 跳转到 Trampoline (虚拟地址固定)
  4. Trampoline 保存用户寄存器到 Trapframe
  5. 切换 satp 寄存器 → 内核页表
  6. 跳转到内核 syscall 处理函数
  7. 执行内核代码 (读取文件等)
  8. 准备返回 → 跳转到 Trampoline 返回代码
  9. 切换 satp 寄存器 → 用户页表
  10. 从 Trapframe 恢复用户寄存器
  11. 执行 sret 返回用户态
  12. 用户程序继续执行

ecall 会将模式设置为 super ,然后将程序计数器保存到 sepc 中,然后将程序计数器设置为 stvec 的寄存器
ecall 所做的事情十分少,是因为 RISC-V 的设计者希望软件有更大的灵活性。

Trampline 的工作流程

Trampline 全流程:

  1. 用户态代码执行
  2. Trap 发生 (系统调用/中断)
  3. CPU 跳转到 Trampoline 入口 (两个页表都有映射)
  4. Trampoline 代码:保存用户寄存器到 Trapframe
  5. 切换页表寄存器 (satp) → 内核页表
  6. 跳转到内核 Trap 处理函数
概念作用类似物
Trap用户→内核的入口函数调用、中断向量
Trapframe保存执行现场快照
Trampoline页表切换的桥梁启动代码 (bootloader)、切换栈的跳板

Trap

Trap 是用户态程序进入内核态的"入口"。 当用户程序需要操作系统服务时,会触发 Trap。

Trap 的三种类型:

类型触发原因例子
系统调用程序主动请求read(), write(), fork()
异常程序出错除零、非法内存访问、缺页
中断外部事件定时器、键盘输入、网络包到达

用户态指令 → Trap 发生 → CPU 自动:

  1. 切换到内核模式 (Kernel Mode)
  2. 跳转到预设的 Trap 处理入口 (stvec 寄存器)
  3. 禁用中断 (可选)

Trapframe

Trapframe 是一个数据结构,用于保存用户程序被中断时的"现场"。 就像拍照一样,保存所有寄存器状态,以便返回时能恢复。
Trapframe 保存所有寄存器,返回时恢复,如果不保存寄存器,用户程序的数据就丢失了!

Trapframe struct (Risk-V)

c
struct trapframe {
  uint64 kernel_satp;   // 内核页表基址
  uint64 kernel_sp;     // 内核栈指针
  uint64 kernel_hartid; // CPU ID
  uint64 ra;            // 返回地址
  uint64 sp;            // 栈指针
  uint64 gp;            // 全局指针
  uint64 tp;            // 线程指针
  uint64 t0;            // 临时寄存器
  uint64 t1;
  uint64 t2;
  uint64 s0;            // 保存寄存器
  uint64 s1;
  uint64 a0;            // 参数/返回值
  uint64 a1;
  // ... a2-a7, s2-s11, t3-t6
  uint64 sepc;          // 保存的程序计数器 (PC)
  uint64 sstatus;       // 保存的状态寄存器
};
  • 存储在内核空间的 proc 结构体中
  • 用户态无法访问(安全)
  • 每个进程有自己的 Trapframe

Trampoline

Trampoline 是一段特殊的代码页,在用户页表和内核页表中都有映射。 它像"跳板"一样,帮助用户态和内核态之间安全切换。

在分离页表场景下:

text
用户态运行 → Trap 发生 → 需要切换到内核页表

但切换页表需要执行代码!

如果当前页表没有内核代码映射,CPU 会崩溃!

解决方案:Trampoline 页在两个页表中都映射
特性说明
双重映射在用户页表和内核页表中映射到相同虚拟地址
位置固定通常在虚拟地址空间顶部 (如 0xffffffffff000000)
代码精简只包含切换页表、保存/恢复寄存器的代码
用户不可执行虽然映射,但用户态无法直接调用