MIT 6.S081 Isolation & System call entry/exit

发布时间 2023-07-12 10:28:10作者: zwyyy456

Trap 机制

程序运行往往需要完成用户空间和内核空间的切换,每当:

  • 程序执行系统调用(system call);
  • 程序出现了 page fault 等错误;
  • 一个设备触发了中断;

都会发生这样的切换。

这里用户空间切换到内核空间通常被称为 trap,因此有时候我们会说程序“陷入”到内核态。trap 机制需要尽可能的简单。

trap 的工作,可以说是让硬件从适合运行用户程序的状态,切换到适合运行内核代码的状态。

这里说的状态中,我们最关心的状态可能是 $32$ 个用户寄存器,我们尤其需要关注以下硬件寄存器的内容:

  • 堆栈寄存器(stack register,又称 stack pointer);
  • 程序计数器(Program Counter Register);
  • 表明当前 mode 的标志位的寄存器,表明当前是 supervisor mode 还是 user mode;
  • 控制 CPU 工作方式的寄存器,例如 SATP(Supervisor Address Translation and Protection)寄存器,它包含了指向 page table 的物理内存地址;
  • STVEC(Supervisor Trap Vector Base Address Register)寄存器,它指向了内核中处理 trap 的指令的起始地址;
  • SEPC(Supervisor Exception Program Counter)寄存器,在 trap 的过程中保存程序计数器的值;
  • SSRATCH(Supervisor Scratch Register)寄存器

在 trap 的最开始,CPU 所有的状态肯定还是在运行用户代码而不是内核代码,在 trap 处理的过程中,我们会逐渐更改状态,或者对状态做一些操作,我们可以设想一下我们需要做哪些操作:

  • 保存 $32$ 个用户寄存器的状态,例如,当响应中断完成后,我们会希望能恢复用户程序的执行,而这些寄存器需要被内核代码所使用,因此,在 trap 之前,我们需要保存这 $32$ 个用户寄存器的内容;
  • 保存 PC 的内容,原因类似于保存 $32$ 个用户寄存器;
  • 将 mode 修改为 supervisor mode;
  • 运行内核代码前,将 SATP 由指向 user page table 修改为指向 kernel page table;

trap 机制不会依赖于 $32$ 个用户寄存器;

supervisor mode 可以实现什么 user mode 不能实现的事情?(其实不多)

  • 读写 SATP、STVEC、SEPC、SSCRATCH 等寄存器;
  • 使用 PTE_U 标志位为 0 的 PTE;

supervisor mode 并不能读写任意物理地址,在 supervisor mode 中,也需要通过 page table 来访问内存,如果一个物理地址映射的虚拟地址并不在当前 SATP 指向的 page table 中,又或者 SATP 指向的 page table 中,PTE_U = 1,那么 supervisor mode 不能使用那个地址。

Trap 代码执行流程

2020 版的课程以 Xv6 的 sh.cgetcmd 中执行的 write 系统调用来说明这个例子(2021 版中,getcmd 转而使用了 fprintf),我这里其实是调试的 echo.c

执行 make CPUS=1 qemu-gdb 以及 gdb-multiarch(注意要配置好 .gdbinit),然后 gdb 中执行 file user/echo.o 以及 b main,将断点打在 user/echo.cmain 函数处,多执行几次 continue,直到 qemu 中的 shell 加载完成,可以执行命令了,输入 echo zwyyy,再在 gdb 的窗口中执行 layout split 以及 continue,函数将停在 main 函数处,从 echo.asm 中我们可以看到 write 系统调用对应的 ECALL 指令所在的地址,为 $\text{0x31c}$,因此我们执行 b *0x31c,然后执行 continue

然后我们打印 PC 的值,正好在 $\text{0x31c}$ 处:

a0,a1,a2 寄存器中的内容是 shell 传递给 write 系统调用的参数,a0 是文件描述符,a1 是 shell 想要写入的字符串的指针,a2 是想要写入的字符数:

在 QEMU 中输入 ctrl + a,再输入 c 可以进入到 QEMU 的 console 中,之后输入 info mem,QEMU 会打印完整的 page table

可以看到最后两个 pte 的 vaddr 非常大,接近虚拟地址的顶端,这两个 page 分别是 trapframe page 和 trampoline page。

之后的实例还是按照 2020 的课程视频来讲,(我自己停在 ecall 执行 si 之后会直接到 ret,进入内核的流程没有显示出来。。。)

ECALL 指令其实只会做三件事:

  • 将代码从 user mode 切换到 supervisor mode;
  • 将 PC 的值保存在 SEPC 寄存器中;
  • 跳转到 STVEC 寄存器指向的指令;

执行 ECALL 之后,程序会位于 trampoline page 的起始位置,即 0xffffff000 这个位置。由于 gdb 的一些奇怪行为,trampoline page 中的第一条执行(即 csrrw 指令已经被执行了:

到目前位置,用户寄存器的值还没有被改变。