预备知识

RSICV指令集基本了解。

了解trap过程中会涉及的几个寄存器(stvec, scause, sscratch, sepc等)

trampoline.s

trampoline顾名思义,实际上就是一个跳板页,其在user space和kernel space映射在同一个虚拟地址上,这么做是为了在切换页表之后,可以正常执行代码。

包含两个部分的代码:

  • uservec。使用汇编编写,用于从用户态切换到内核态并且执行usertrap()函数。代码注释非常详尽,主要分为三步:保存现场;切换到内核态;跳转到usertrap()执行。首先交换sscratch和a0寄存器的值,此时的a0寄存器存放的就是trapframe的基址,,接下来将所有的寄存器的值存入trapframe中(后续userret需要恢复);然后通过trapframe,恢复sp、tp等寄存器为内核态的值,最重要的是设置satp(相当于x86-64中的cr3,存放页表基地址),并切换到内核页表;最后跳转到usertrap,函数地址也是存放在trapframe中的。
  • userret。与uservec相对应,从usertrapret()函数中调用,有两个参数,分别是TRAPFRAME地址以及用户态的satp,用于返回user space。首先,切换页表(trapframe是在user space 中映射的);接下来,从trapframe中恢复用户态上下文,这里有一个细节需要注意:由于a0已经存放了TRAPFRAME,所以需要先使用sscratch保存a0,最后再交换回来(a1尽管保存了Satp,但是一开始就已经使用过了,所以可以直接overwrite)。最后,由于usertrapret()中已经设置好了epc,直接使用sret指令返回即可。

trap.c

通过trampoline.s的分析,我们知道trampoline.s中的两个函数实际上就是入口和出口,具体执行的代码位于trap.c中,主要是usertrap(),kerneltrap(),usertrapret()三个函数。

  • usertrap()只能通过用户态陷入内核态的时候调用。首先内核会检查是否从用户态陷入,然后设置stvec为kernelvec()(因为如果现在再次发生了中断,那么不需要再切换页表了,只需要像普通的函数调用一样保存现场);接着根据trap的类型,判断应该如何处理:系统调用、设备中断、异常情况(直接kill);最后,如果没有被kill,那么调用usertrapret()返回用户态。

    intr_on的调用时机

    源代码中在确认了sstatus等寄存之后才开中断,是为了防止中途出现其他trap破坏了这些寄存器。