今天讓一個加法算到一半,先觸發例外,再回來完成運算。a0 保存 25,t0 保存 17,handler 會在中途列印診斷資訊,最後我們仍應得到 42。
如果只讓 CPU 跳到下一條指令,卻沒有恢復原本的暫存器,這個結果很可能就變了。
這正是陷阱框架(Trap Frame)要保存的現場。
《Operating System Concepts》第 10 版第 1.2.1.1 節指出,處理事件時修改的處理器狀態,必須在恢復原工作前還原。
對原程式來說,handler 應該完成自己的工作,同時保留原本計算所需的資料。
普通函式呼叫可以事先依 ABI 保存會被改寫的值,但 trap 可能出現在程式沒有安排呼叫的地方。
因此不能只保存 ABI 規定的 callee-saved registers,就期待所有被打斷的計算都能恢復。
本次 RV32IMAC 沒有浮點或向量執行狀態,我們先保存整數暫存器、sepc 與 sstatus。
CPU 會更新 trap 相關 CSR,但不會自動替我們把所有一般暫存器推入堆疊。
這一部分由組合語言入口完成。
接續 Day 07,在 30-days-os-kernel/tiny-kernel/ 新增 trap.h:
#pragma once
#include "kernel.h"
struct trap_frame {
uint32_t x[32];
uint32_t sepc;
uint32_t sstatus;
uint32_t padding[2];
};
_Static_assert(sizeof(struct trap_frame) == 144, "trap frame size");
_Static_assert(offsetof(struct trap_frame, sepc) == 128, "sepc offset");
_Static_assert(offsetof(struct trap_frame, sstatus) == 132, "sstatus offset");
void trap_entry(void);
void handle_trap(struct trap_frame *frame);
unsigned int trap_test(void);
x[10] 對應 a0,x[5] 對應 t0,x[2] 存放 trap 發生前的 sp。x[0] 雖然永遠是零,仍保留一格,讓暫存器編號直接對應陣列索引。
總大小補到 144 bytes,也就是 16 的倍數,進入 C handler 時保持堆疊對齊。
新增 trap.S:
.section .text
.balign 4
.globl trap_entry
.type trap_entry, @function
trap_entry:
addi sp, sp, -144
sw zero, 0(sp)
sw ra, 4(sp)
sw gp, 12(sp)
sw tp, 16(sp)
sw t0, 20(sp)
sw t1, 24(sp)
sw t2, 28(sp)
sw s0, 32(sp)
sw s1, 36(sp)
sw a0, 40(sp)
sw a1, 44(sp)
sw a2, 48(sp)
sw a3, 52(sp)
sw a4, 56(sp)
sw a5, 60(sp)
sw a6, 64(sp)
sw a7, 68(sp)
sw s2, 72(sp)
sw s3, 76(sp)
sw s4, 80(sp)
sw s5, 84(sp)
sw s6, 88(sp)
sw s7, 92(sp)
sw s8, 96(sp)
sw s9, 100(sp)
sw s10,104(sp)
sw s11,108(sp)
sw t3, 112(sp)
sw t4, 116(sp)
sw t5, 120(sp)
sw t6, 124(sp)
addi t0, sp, 144
sw t0, 8(sp)
csrr t0, sepc
sw t0, 128(sp)
csrr t0, sstatus
sw t0, 132(sp)
mv a0, sp
call handle_trap
lw t0, 128(sp)
csrw sepc, t0
lw t0, 132(sp)
csrw sstatus, t0
lw ra, 4(sp)
lw gp, 12(sp)
lw tp, 16(sp)
lw t1, 24(sp)
lw t2, 28(sp)
lw s0, 32(sp)
lw s1, 36(sp)
lw a0, 40(sp)
lw a1, 44(sp)
lw a2, 48(sp)
lw a3, 52(sp)
lw a4, 56(sp)
lw a5, 60(sp)
lw a6, 64(sp)
lw a7, 68(sp)
lw s2, 72(sp)
lw s3, 76(sp)
lw s4, 80(sp)
lw s5, 84(sp)
lw s6, 88(sp)
lw s7, 92(sp)
lw s8, 96(sp)
lw s9, 100(sp)
lw s10,104(sp)
lw s11,108(sp)
lw t3, 112(sp)
lw t4, 116(sp)
lw t5, 120(sp)
lw t6, 124(sp)
lw t0, 20(sp)
lw sp, 8(sp)
sret
.size trap_entry, . - trap_entry
入口先移動 sp 保留空間,保存原本的 t0 後,才借用 t0 計算舊堆疊位址與搬運 CSR。
返回時最後才還原 sp,否則中途改了基準位址,後面的 lw 就會讀錯位置。
此版本只處理發生在有效 S-mode 核心堆疊上的 trap,不處理 U-mode 堆疊切換或巢狀例外。
我們不在 handler 裡重新開啟 S-mode 中斷,遇到未預期事件也仍然 Panic。
到使用者模式篇,必須再設計如何從使用者堆疊換到可信任的核心堆疊。
新增 trap_test.S:
.section .text
.balign 4
.globl trap_test
.type trap_test, @function
trap_test:
li t0, 17
li a0, 25
.globl expected_fault
expected_fault:
csrr a0, mstatus
.globl expected_resume
expected_resume:
add a0, a0, t0
ret
.size trap_test, . - trap_test
讀取 mstatus 失敗時,原本的 a0 應保持 25。
我們把故障指令和恢復位置都取名,handler 就能精確辨認這個教學實驗。
新增 trap.c:
#include "trap.h"
#include "csr.h"
extern char expected_fault[], expected_resume[];
void handle_trap(struct trap_frame *frame) {
unsigned int cause = READ_CSR(scause);
printf("trap cause=%u saved a0=%u t0=%u\n",
cause, frame->x[10], frame->x[5]);
if (cause != 2 || frame->sepc != (uint32_t) expected_fault)
PANIC("unexpected trap");
KASSERT(frame->x[10] == 25);
KASSERT(frame->x[5] == 17);
frame->sepc = (uint32_t) expected_resume;
}
這不是把所有非法指令當作可忽略錯誤。
只有原因碼和位址都符合受控測試時,才設定下一個執行位置,其餘事件停下來處理。
明確指定符號也避免把「每個 trap 都把 sepc 加四」寫成通則,RISC-V 指令長度與不同例外的處理策略並不相同。
Makefile 改用新的檔案,Day 07 的 trap_stop.S 與 trap_stop.c 可留作歷史實驗,但不再加入 SOURCES:
SOURCES := kernel.c asm_probe.S boot.S console.c panic.c trap.S trap.c trap_test.S
在 kernel.c 加入 #include "trap.h",移除不用的 trap_stop_entry 宣告,並取代 kernel_main():
void kernel_main(void) {
WRITE_CSR(stvec, (unsigned int) trap_entry);
unsigned int result = trap_test();
KASSERT(result == 42);
printf("resumed: result=%u\n", result);
for (;;)
__asm__ __volatile__("wfi");
}
在 tiny-kernel/ 執行 make 與 make run,預期看到:
trap cause=2 saved a0=25 t0=17
resumed: result=42
要觀察保存區,可使用 Day 03 的除錯啟動方式,在 GDB 停到 handle_trap:
target remote 127.0.0.1:1234
hbreak *handle_trap
continue
set $frame = (struct trap_frame *) $a0
p/x $frame->sepc
p/u $frame->x[10]
p/u $frame->x[5]
p/x $frame->x[2]
如果反覆印出同一個原因與位址,檢查 frame->sepc 是否真的被寫回 CSR。
如果能返回但結果不對,檢查 C 結構與組語 offset,尤其是拿來傳入 handler 參數的 a0 和作為暫存的 t0。
今天新增 trap.h、trap.S、trap.c 與 trap_test.S,修改 kernel.c 與 Makefile。
建議 commit 訊息為:
day08: implement trap context save
Day 09 開始管理可重複使用的 RAM,讓後面的行程堆疊與分頁表有地方可以配置。