iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 7

Day 07|Exception 與 Trap:CPU 如何突然跳進 Kernel Handler?

  • 分享至 

  • xImage
  •  

今天在 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 的一般中斷沒有打開,這條違規指令仍然能觸發例外。

先給 CPU 一個明確入口

《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.ckernel_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 函式呼叫不同。

對照 Console 與反組譯

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,而且低兩個位元都是零。

看不到 handler 時,沿路找交接點

如果停在 before illegal CSR read,可以用 GDB 對 trap_stop_entrytrap_stop 分別設定硬體斷點。
前者沒有命中,要檢查 stvec 是否正確,以及韌體是否把這個例外交給 S-mode。
前者命中但後者沒命中,則優先檢查診斷堆疊與組語呼叫 C 的位置。

不要因為沒看到訊息就直接把 sepc 往後加四,今天的入口根本沒有可恢復的現場。
先確定例外能被辨識並回報,下一篇再把返回行為建立起來。

今天新增 trap_stop.Strap_stop.c,修改 kernel.c 與 Makefile。
這一天保留刻意觸發例外的展示版本,建議 commit 訊息為:

day07: implement trap handler

Day 08 會保存一般暫存器與返回狀態,讓同一個受控實驗在 handler 結束後繼續計算。

參考資料


上一篇
Day 06|Kernel Panic:核心出錯時怎麼停下來並找出問題?
下一篇
Day 08|Trap Frame:進 Kernel 前 CPU 的暫存器去了哪裡?
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言