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.c
的 getcmd
中执行的 write
系统调用来说明这个例子(2021 版中,getcmd
转而使用了 fprintf
),我这里其实是调试的 echo.c
。
执行 make CPUS=1 qemu-gdb
以及 gdb-multiarch
(注意要配置好 .gdbinit
),然后 gdb 中执行 file user/echo.o
以及 b main
,将断点打在 user/echo.c
的 main
函数处,多执行几次 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
指令已经被执行了:
到目前位置,用户寄存器的值还没有被改变。
标签:内核,Isolation,System,exit,mode,寄存器,table,page,trap From: https://www.cnblogs.com/zwyyy456/p/17546849.html