昨天核心已經把使用者程式放進記憶體,但還沒有執行它。
今天要讓終端機出現 U-mode says hello,而且這段文字必須由 U-mode 程式提出輸出請求,再由核心完成。
同樣是印字,這次多了一條權限邊界。
使用者程式不能直接呼叫核心內部的 putchar(),我們要替它設計一個受控入口。
《Operating System Concepts》第 10 版第 2.3 節介紹系統呼叫:應用程式透過約定的介面請求作業系統服務,核心依呼叫編號與參數選擇處理方式。
這個概念今天會落到三個地方:使用者程式的 ecall、Trap Frame 中的參數,以及核心的 dispatcher。
一般 C 函式呼叫只改變控制流程,不會自行把 U-mode 變成 S-mode。
RISC-V 的 ecall 則會觸發例外,CPU 依權限與委派設定進入對應的處理入口。
在本系列使用的 OpenSBI 環境中,U-mode 的 environment call 由我們的 S-mode 核心處理,scause 為 8。
這和核心向 OpenSBI 求助的 SBI 呼叫不同:前者是 U → S,後者是 S → M,不能只因為兩者都使用 ecall 就混為一談。
今天不採用 Linux 的 syscall 編號,也不宣稱符合 POSIX。
我們只需要一份能由兩端共同遵守的約定:
| 暫存器/編號 | 意義 |
|---|---|
a7 |
系統呼叫編號 |
a0 |
第一個參數,返回時也是結果 |
| 編號 1 | 輸出 a0 的低 8 位元,成功回傳 0 |
| 編號 2 | 印出結束碼,停止這次單一使用者程式實驗 |
| 未知編號 | 回傳 0xffffffff,視為有號整數時是 −1 |
例如 user.S 中的輸出請求相當於:
li a0, 'H'
li a7, 1
ecall
程式會先呼叫不存在的編號 999,確認錯誤回傳值,再逐字印出文字。
這樣我們不只測成功路徑,也能確認 dispatcher 沒有把未知編號當成有效功能。
完整入口位於 user_entry.S。
呼叫端傳入使用者入口、使用者堆疊頂端,以及獨立核心堆疊的頂端:
csrw sepc, a0
csrw sscratch, a2
li t0, 0x40122
csrc sstatus, t0
mv sp, a1
sret
sepc 指向 Day 16 載入的程式,sret 的目的權限由 sstatus.SPP 決定。
這裡清除 SPP,讓返回目標成為 U-mode。
遮罩還清除了 SUM、SPIE 與 SIE。
本次尚未啟用非同步中斷,核心也不依靠 SUM 直接讀寫使用者映射,因此要把這些條件明確固定下來。
這不是可直接套用到完整搶占式核心的通用初始化範本。
進入 Trap 時,CPU 不會替我們自動換成核心堆疊,也不會保存全部通用暫存器。
如果立刻在使用者的 sp 上建立 Trap Frame,等於讓使用者決定核心要把重要狀態存在哪裡。
因此 trap.S 採用以下約定:
sscratch 保存核心堆疊頂端sscratch 為 0sp 與 sscratch,依交換結果區分入口情況來自 U-mode 時,新的 sp 就是核心堆疊,原本的 user sp 則暫存在 sscratch。
入口接著保存所有需要恢復的通用暫存器,以及 sepc 與 sstatus,再呼叫 C handler。
返回前的順序也很重要:先準備下一次 Trap 所需的 sscratch,恢復其他暫存器,最後才恢復 user sp 並執行 sret。
這份範例沒有巢狀例外復原機制,核心內出現意外 Trap 會停止並列出診斷資訊。
閱讀 user.c 的 handle_trap(),最前面的檢查是:
unsigned int cause = READ_CSR(scause);
if (cause != 8 || (f->sstatus & 256)) {
printf("trap cause=%u sepc=0x%x stval=0x%x\n",
cause, f->sepc, READ_CSR(stval));
PANIC("unexpected trap");
}
unsigned int id = f->x[17];
f->sepc += 4;
x[17] 對應 a7,x[10] 對應 a0。
核心修改 Trap Frame 中的 a0,返回組語就會把結果帶回使用者程式。
這裡能將 sepc 增加 4,是因為已確認目前處理的是固定 32-bit 長度的 ecall。
不能把同樣做法套在每一種例外,尤其這個 RV32IMAC 專案也允許 16-bit 壓縮指令。
若完全不調整 sepc,返回後又會執行同一個 ecall,看起來就像核心不斷收到相同請求。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=17 run
頁表檢查訊息之後,預期看到:
enter U-mode
U-mode says hello
user exit=0
這段輸出代表使用者程式通過未知編號測試,成功完成多次字元輸出,再帶著結束碼 0 回到核心。
編號 2 目前會讓整個實驗停在 halt(),不是完整的行程終止與排程交接。
結束 QEMU 可按 Ctrl+A 後按 X。
另外確認使用者映像真的含有系統呼叫指令:
llvm-objdump -d build/day17/user.elf
找到未知編號測試,以及輸出迴圈中的 ecall。
若輸出停在 enter U-mode,優先檢查 Trap 診斷、程式頁的 U/X 權限,以及 user sp 是否落在可寫頁面。
這次的完整改動集中在 user_entry.S、trap.S、user.c 與 user.S。
範例仍是單 hart、單一使用者程式,Day 12 的多行程排程器尚未與使用者位址空間整合。
建議 commit:
day17: enter user mode and dispatch system calls
下一篇把同樣的請求路徑接到磁碟,讓使用者程式拿到的文字不再藏在自己的程式映像中。