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 来使得这里的处理流程更加简单,更加快速。
  • 另一个需要时刻记住的问题是,恶意软件是否能滥用这里的机制来打破隔离性。