实现上下文环境保存与恢复
OS 在正确完成中断初始化(设置中断处理程序的起始地址,并使能中断)后,还需为被中断的程序保存和恢复当时程序运行时的上下文(实际上就是一堆寄存器的值,具体内容在part3 中描述的中断帧(TrapFrame)结构体中有详细定义)
1. # src/trap/trap.asm
3. .section .text
4. .globl __alltraps
5. .align 4
6. __alltraps:
7. SAVE_ALL
8. mv a0, sp
9. jal rust_trap
11. .globl __trapret
12. __trapret:
13. RESTORE_ALL
14. sret
我们首先定义 __alltraps 函数作为所有中断处理程序的入口,这里我们首先通过 SAVE_ALL (汇编宏)来保存上下文环境,随后将当前栈顶地址 sp 的值给到寄存器 a0 ,这是因为在 risc-v calling convention 中,规定 a0 保存函数输入的第一个参数,于是就相当于将栈顶地址传给函数 rust_trap 作为第一个参数。正如我们之前提及的,__alltraps 需要以四字节对齐。
随后,我们通过 jal 调用 rust_trap 函数并在返回之后跳转到调用语句的下一条指令。实际上调用返回之后进入 __trapret 函数,这里我们通过 RESTORE_ALL (汇编宏)恢复中断之前的上下文环境,并最终通过一条 sret 指令跳转到 sepc指向的地址,即回到触发中断的那条指令所在地址。这会导致触发中断的那条指令又被执行一次。
注意,由于这部分用到了 SAVE_ALL 和 RESTORE_ALL 两个汇编宏,所以这部分必须写在最下面。
我们定义几个常量和宏:
1. # src/trap/trap.asm
3. # 常量:表示每个寄存器占的字节数,由于是64位,都是8字节
4. .equ XLENB, 8
6. # 将地址 sp+8*a2 处的值 load 到寄存器 a1 内
7. .macro LOAD a1, a2
8. ld \a1, \a2*XLENB(sp)
9. .endm
11. # 将寄存器 a1 内的值 store 到地址 sp+8*a2 内
12. .macro STORE a1, a2
13. sd \a1, \a2*XLENB(sp)
14. .endm
保存上下文环境
SAVE_ALL 的原理是:将一整个 TrapFrame 保存在内核栈上。我们现在就处在内核态(S 态),因此现在的栈顶地址 sp 就指向内核栈地址。但是,之后我们还要支持运行用户态程序,顾名思义,要在用户态(U 态)上运行,在中断时栈顶地址 sp 将指向用户栈顶地址,这种情况下我们要从用户栈切换到内核栈。
1. # src/trap/trap.asm
3. # 规定若在中断之前处于 U 态(用户态)
4. # 则 sscratch 保存的是内核栈地址
5. # 否则中断之前处于 S 态(内核态),sscratch 保存的是 0
6. .macro SAVE_ALL
7. # 通过原子操作交换 sp, sscratch
8. # 实际上是将右侧寄存器的值写入中间 csr
9. # 并将中间 csr 的值写入左侧寄存器
10. csrrw sp, sscratch, sp
12. # 如果 sp=0 ,说明交换前 sscratch=0
13. # 则说明从内核态进入中断,不用切换栈
14. # 因此不跳转,继续执行 csrr 再将 sscratch 的值读回 sp
15. # 此时 sp,sscratch 均保存内核栈
17. # 否则 说明sp!=0,说明从用户态进入中断,要切换栈
18. # 由于 sscratch 规定,二者交换后
19. # 此时 sp 为内核栈, sscratch 为用户栈
20. # 略过 csrr 指令
22. # 两种情况接下来都是在内核栈上保存上下文环境
23. bnez sp, trap_from_user
24. trap_from_kernel:
25. csrr sp, sscratch
26. trap_from_user:
27. # 提前分配栈帧
28. addi sp, sp, -36*XLENB
29. # 按照地址递增的顺序,保存除x0, x2之外的通用寄存器
30. # x0 恒为 0 不必保存
31. # x2 为 sp 寄存器,需特殊处理
32. STORE x1, 1
33. STORE x3, 3
34. STORE x4, 4
35. ...
36. STORE x30, 30
37. STORE x31, 31
39. # 若从内核态进入中断,此时 sscratch 为内核栈地址
40. # 若从用户态进入中断,此时 sscratch 为用户栈地址
41. # 将 sscratch 的值保存在 s0 中,并将 sscratch 清零
42. csrrw s0, sscratch, x0
43. # 分别将四个寄存器的值保存在 s1,s2,s3,s4 中
44. csrr s1, sstatus
45. csrr s2, sepc
46. csrr s3, stval
47. csrr s4, scause
49. # 将 s0 保存在栈上
50. STORE s0, 2
51. # 将 s1,s2,s3,s4 保存在栈上
52. STORE s1, 32
53. STORE s2, 33
54. STORE s3, 34
55. STORE s4, 35
56. .endm
在 SAVE_ALL 之后,我们将一整个 TrapFrame 存在了内核栈上,且在地址区间

上按照顺序存放了 TrapFrame 的各个字段。这样,rust_trap 可以通过栈顶地址正确访问 TrapFrame 了。
恢复上下文环境
而 RESTORE_ALL 正好是一个反过来的过程:
1. # src/trap/trap.asm
3. .macro RESTORE_ALL
4. # s1 = sstatus
5. LOAD s1, 32
6. # s2 = sepc
7. LOAD s2, 33
8. # 我们可以通过另一种方式判断是从内核态还是用户态进入中断
9. # 如果从内核态进入中断, sstatus 的 SPP 位被硬件设为 1
10. # 如果从用户态进入中断, sstatus 的 SPP 位被硬件设为 0
11. # 取出 sstatus 的 SPP
12. andi s0, s1, 1 << 8
13. # 若 SPP=0 , 从用户态进入中断,进行 _to_user 额外处理
14. bnez s0, _to_kernel
15. _to_user:
16. # 释放在内核栈上分配的内存
17. addi s0, sp, 36 * XLENB
18. # RESTORE_ALL 之后,如果从用户态进入中断
19. # sscratch 指向用户栈地址!
20. # 现在令 sscratch 指向内核栈顶地址
21. # 如果是从内核态进入中断,在 SAVE_ALL 里面
22. # 就把 sscratch 清零了,因此保证了我们的规定
23. csrw sscratch, s0
24. _to_kernel:
25. # 恢复 sstatus, sepc 寄存器
26. csrw sstatus, s1
27. csrw sepc, s2
29. # 恢复除 x0, x2(sp) 之外的通用寄存器
30. LOAD x1, 1
31. LOAD x3, 3
32. LOAD x4, 4
33. ...
34. LOAD x31, 31
36. # 如果从用户态进入中断, sp+2*8 地址处保存用户栈顶地址
37. # 切换回用户栈
38. # 如果从内核态进入中断, sp+2*8 地址处保存内核栈顶地址
39. # 切换回内核栈
40. LOAD x2, 2
41. .endm
现在是时候实现中断处理函数 rust_trap了!
1. // src/interrupt.rs
3. // 引入 TrapFrame 结构体
4. use crate::context::TrapFrame;
6. // 载入 trap.asm
7. global_asm!(include_str!("trap/trap.asm"));
9. pub fn init() {
10. unsafe {
11. extern "C" {
12. // 中断处理总入口
13. fn __alltraps();
14. }
15. // 经过上面的分析,由于现在是在内核态
16. // 我们要把 sscratch 初始化为 0
17. sscratch::write(0);
18. // 仍使用 Direct 模式
19. // 将中断处理总入口设置为 __alltraps
20. stvec::write(__alltraps as usize, stvec::TrapMode::Direct);
21. }
22. println!("++++ setup interrupt! ++++");
23. }
25. // 删除原来的 trap_handler ,改成 rust_trap
26. // 以 &mut TrapFrame 作为参数,因此可以知道中断相关信息
27. // 在这里进行中断分发及处理
28. #[no_mangle]
29. pub fn rust_trap(tf: &mut TrapFrame) {
30. println!("rust_trap!");
31. // 触发中断时,硬件会将 sepc 设置为触发中断指令的地址
32. // 而中断处理结束,使用 sret 返回时也会跳转到 sepc 处
33. // 于是我们又要执行一次那条指令,触发中断,无限循环下去
34. // 而我们这里是断点中断,只想这个中断触发一次
35. // 因此我们将中断帧内的 sepc 字段设置为触发中断指令下一条指令的地址,即中断结束后跳过这条语句
36. // 由于 riscv64 的每条指令都是 32 位,4 字节,因此将地址+ 4 即可
37. // 这样在 RESTORE_ALL 时,这个修改后的 sepc 字段就会被 load 到 sepc 寄存器中
38. // 使用 sret 返回时就会跳转到 ebreak 的下一条指令了
39. tf.sepc += 4;
40. }
看起来很对,那我们 make run 运行一下吧!
infinite rust_trap
结果却不尽如人意,输出了一大堆乱码!
我们使用 make asm 检查一下生成的汇编代码,看看是不是哪里出了问题。找到我们手动触发中断的 ebreak 指令:
1. ...
2. 0000000080200010 rust_main:
3. 80200010: 01 11 addi sp, sp, -32
4. 80200012: 06 ec sd ra, 24(sp)
5. 80200014: 22 e8 sd s0, 16(sp)
6. 80200016: 00 10 addi s0, sp, 32
7. 80200018: 97 00 00 00 auipc ra, 0
8. 8020001c: e7 80 40 10 jalr 260(ra)
9. 80200020: 09 a0 j 2
10. 80200022: 02 90 ebreak
12. 0000000080200024 .LBB0_3:
13. 80200024: 17 35 00 00 auipc a0, 3
14. ...
不是说 riscv64 里面每条指令长度为 4 字节吗?我们发现 ebreak 这条指令仅长为 2 字节。我们将 ebreak 所在的地址 +4 ,得到的甚至不是一条合法指令的开头,而是下一条指令正中间的地址!这样当然有问题了。
我们回头来看 riscv64 目标三元组中的一个设置:
1. "features": "+m,+a,+c",
实际上,这表示指令集的拓展。+m 表示可以使用整数乘除法指令; +a 表示可以使用原子操作指令; +c 表示开启压缩指令集,即对于一些常见指令,编译器会将其压缩到

位即

字节,来降低可执行文件的大小!这就出现了上面那种诡异的情况。
所以我们只需将 sepc 修正为 +2:
1. - tf.sepc += 4;
2. + tf.sepc += 2;
再 make run 尝试一下:
back from trap
1. ++++ setup interrupt! ++++ 2. rust_trap! 3. panicked at 'end of rust_main', src/init.rs:9:5
可以看到,我们确实手动触发中断,调用了中断处理函数,并通过上下文保存与恢复机制保护了上下文环境不受到破坏,正确在 ebreak 中断处理程序返回之后 panic。
迄今为止的代码可以在这里找到。如果出现了问题的话就来检查一下吧。
