今天在 S-mode 執行一條讀取 mstatus 的指令。
這個 CSR 需要 M-mode 權限,因此我們預期 CPU 拒絕執行,並讓核心印出 Illegal Instruction 的原因碼 2。
Day 03 先讀取合法的狀態,現在才加入違規測試,是因為我們終於能為它安排處理入口與錯誤輸出。
這次要看見的是 CPU 主動移交控制權的過程。
《Operating System Concepts》第 10 版第 1.2.1 節,說明事件如何讓 CPU 暫停原本工作,轉入處理程式並保存需要的狀態。
這個概念在 RISC-V 裡要進一步分清兩類事件。
例外(Exception)與正在執行的指令有關,例如非法指令或環境呼叫。
中斷(Interrupt)則由計時器或裝置等事件提出,不是這條程式指令本身出錯。
RISC-V 使用 Trap 統稱因例外或中斷造成的控制權轉移,和某些一般教材對 trap 的較窄用法不同。
今天只處理一個同步例外,不啟用計時器中斷。
所以即使 S-mode 的一般中斷沒有打開,這條違規指令仍然能觸發例外。
《OS in 1,000 Lines》的〈例外處理〉介紹 stvec 與狀態保存。stvec 是 Supervisor Trap Vector,指定轉入 S-mode trap 時從哪裡執行。
本次使用 Direct 模式,所有送到 S-mode 的 trap 都從同一個入口開始。
不是每一種事件都必然交給 S-mode,M-mode 的委派設定也會影響路徑。
以下實驗使用 Day 01 指定的 OpenSBI 韌體,依它的例外委派設定驗證 Illegal Instruction。
在 30-days-os-kernel/tiny-kernel/ 新增 trap_stop.S:
.section .bss
.balign 16
.Ltrap_stack:
.space 4096
.Ltrap_stack_top:
.section .text
.balign 4
.globl trap_stop_entry
.type trap_stop_entry, @function
trap_stop_entry:
la sp, .Ltrap_stack_top
csrr a0, scause
csrr a1, sepc
csrr a2, stval
call trap_stop
1:
j 1b
.size trap_stop_entry, . - trap_stop_entry
這個入口切到預留的 4 KiB 診斷堆疊,然後讀取三個 CSR 並呼叫 C handler。
它是只負責回報並停止的版本,已經捨棄原本的 sp,也沒有保存其他一般暫存器,因此不能從這裡直接 sret。
Day 08 才會換成可以返回原程式的入口。
堆疊與入口分別以 16 bytes 和 4 bytes 對齊,對應 C 呼叫慣例與 stvec 位址要求。
這份簡化版本只供單核心、單次診斷使用,不支援巢狀 trap。
新增 trap_stop.c:
#include "kernel.h"
__attribute__((noreturn))
void trap_stop(unsigned int cause, unsigned int epc, unsigned int value) {
printf("trap: scause=0x%x sepc=0x%x stval=0x%x\n",
cause, epc, value);
PANIC("unhandled supervisor trap");
}
scause 提供原因,RV32 的最高位元用來區分中斷與例外,剩下的位元才是原因碼。sepc 是 trap 保存的程式位置,這次應該指向違規讀取 mstatus 的指令。stval 提供依例外種類而定的補充資訊,不保證每一種情況都能讀到有效地址或完整指令。
Makefile 的 SOURCES 更新為:
SOURCES := kernel.c asm_probe.S boot.S console.c panic.c trap_stop.S trap_stop.c
在 kernel.c 的 kernel_main() 前加入:
extern void trap_stop_entry(void);
再用以下版本取代 kernel_main():
void kernel_main(void) {
WRITE_CSR(stvec, (unsigned int) trap_stop_entry);
printf("stvec=0x%x\n", READ_CSR(stvec));
printf("before illegal CSR read\n");
__asm__ __volatile__("csrr a0, mstatus" : : : "a0", "memory");
printf("unexpected: illegal CSR read returned\n");
for (;;)
__asm__ __volatile__("wfi");
}
這裡沒有任何地方呼叫 trap_stop_entry(),只把位址寫進 CSR。
真正進入它的動作由 CPU 在 trap 路徑中完成,這和 Day 06 的普通 C 函式呼叫不同。
在 tiny-kernel/ 執行:
make
make run
預期核心輸出順序為:
stvec=0x...
before illegal CSR read
trap: scause=0x2 sepc=0x... stval=0x...
PANIC trap_stop.c:行號: unhandled supervisor trap
unexpected 不應出現。
結束 QEMU 後,對照本次 ELF:
llvm-objdump -d --disassemble-symbols=kernel_main build/kernel.elf
llvm-nm -n build/kernel.elf
找出 csrr a0, mstatus 的指令位址,應與 Console 的 sepc 相同。
再用符號表確認 stvec 對應 trap_stop_entry,而且低兩個位元都是零。
如果停在 before illegal CSR read,可以用 GDB 對 trap_stop_entry 與 trap_stop 分別設定硬體斷點。
前者沒有命中,要檢查 stvec 是否正確,以及韌體是否把這個例外交給 S-mode。
前者命中但後者沒命中,則優先檢查診斷堆疊與組語呼叫 C 的位置。
不要因為沒看到訊息就直接把 sepc 往後加四,今天的入口根本沒有可恢復的現場。
先確定例外能被辨識並回報,下一篇再把返回行為建立起來。
今天新增 trap_stop.S 與 trap_stop.c,修改 kernel.c 與 Makefile。
這一天保留刻意觸發例外的展示版本,建議 commit 訊息為:
day07: implement trap handler
Day 08 會保存一般暫存器與返回狀態,讓同一個受控實驗在 handler 結束後繼續計算。