本節是以 Golang 上游 ee91bb83198f61aa8f26c3100ca7558d302c0a98 為基準做的實驗。
予焦啦!回顧第零章,我們有了一支可以充分支援開發的工具鏈,也建立了模擬器的使用經驗。開機啟動流程上了軌道,我們已經確實進入到 ethanol 的映像檔了。並且,就在昨日,我們導入早期錯誤處理用的微程式,成功捕捉第一個未知的錯誤。
然而,在真正開始開發任何認真的功能之前,為了避免除錯所需的時間耗損,我們還是先準備一些除錯工具和技能吧!
scause
sepc
stval
仔細想想,到昨日為止,我們成功完成的事情也就只是卡在 ethanol 核心最一開始的地步。雖然卡住了,但也沒有辦法回答這些問題:
early_halt
?但有一就有二,我們能印出一個字元,理論上就能夠印出更多,提供更全面的訊息。以上三個問題,都是很理所當然的基本問題,RISC-V 當然也是支援的。事實上,以上三個問題的解答,可以分別從三個狀態暫存器取得:scause
、sepc
以及 stval
。
為什麼先前都說控制暫存器,現在又說狀態暫存器?呃,因為 CSR 的意義就是控制與狀態暫存器,但為了描述三個字母的一個名詞而用上八個字實在是太多了。之後會依場合決定如何稱呼,但請讀者知悉,控制暫存器與狀態暫存器在本系列文中都用以指涉 CSR。
我們先分別將這三個狀態暫存器(編號都是來自權限指令集規格書)導入既有的框架中吧:
$ git diff
diff --git a/src/runtime/opensbi/csr.h b/src/runtime/opensbi/csr.h
index 40edffbba1..2ee6d54498 100644
--- a/src/runtime/opensbi/csr.h
+++ b/src/runtime/opensbi/csr.h
@@ -3,4 +3,7 @@
// license that can be found in the LICENSE file.
// CSR encoding
#define CSR_STVEC $0x105
+#define CSR_SEPC $0x141
+#define CSR_SCAUSE $0x142
+#define CSR_STVAL $0x143
而且,我們仍需應用 csrrs
來生成虛擬指令 csrr
的純粹讀取狀態暫存器的效果。如下:
diff --git a/src/cmd/internal/obj/riscv/obj.go b/src/cmd/internal/obj/riscv/obj.go
index 6180f6be79..fcb953d03d 100644
--- a/src/cmd/internal/obj/riscv/obj.go
+++ b/src/cmd/internal/obj/riscv/obj.go
@@ -1686,6 +1686,7 @@ var encodings = [ALAST & obj.AMask]encoding{
AEBREAK & obj.AMask: iIEncoding,
AWFI & obj.AMask: iIEncoding,
ACSRRW & obj.AMask: iIEncoding,
+ ACSRRS & obj.AMask: iIEncoding,
有了這些工具之後,我們還欠缺一個關鍵,那就是能夠將取得的資料轉換為一個一個 ASCII 碼,再比照使用先前的控制台輸出方法進行輸出。
也許熟悉 Golang 的讀者想問,為何不能呼叫
fmt.Println
或是其他runtime
組件絕對先於fmt
組件,所以現階段不可能使用fmt
系列呼叫;二來就算接通了,也還沒有控制台或是任何輸出裝置的驅動程式可使用。
但也不必做得太通用。既然我們現在只要看到狀態暫存器內容就可滿足,那麼就設計一個會執行 16 次的迴圈,每次印出一個 16 進位碼即可。也就是說,將 A1
暫存器設為 15(X0
恆為 0),每次迴圈內容結束後減一,若是仍然大於或等於(BGE,Branch if Greater or Equal to)0 的話,則回到 loop
符號,否則就繼續往下執行。這個骨架的實作為:
#include "opensbi/csr.h"
+TEXT dump(SB),NOSPLIT|NOFRAME,$0
+ ADDI $15, X0, A1
+loop:
+ ADDI $-1, A1, A1
+ BGE A1, X0, loop
+
TEXT early_halt(SB),NOSPLIT|NOFRAME,$0
A1
只能算是一個迴圈變數,這個東西要有用的話,應該要是每 4 個位元一組,先從目標 64 位元數字當中抽取出來。做法有很多,這裡是先計算出每一次需要處理的最低位元索引(A1 乘以 4,也就是左移 2),然後將這 4 個位元先對齊到最低位,再用 0xF
作為位元遮罩取得目標的 4 個位元,存在 A0
:
+// the input is in A0
TEXT dump(SB),NOSPLIT|NOFRAME,$0
ADDI $15, X0, A1
loop:
+ // calculate the lowest bit to preserve in A0
+ SLLI $2, A1, A2
+ SRL A2, A0, A3
+ // we only need 4 bits
+ ADDI $0xF, X0, A2
+ AND A3, A2, A0
+
+ // the end of loop
ADDI $-1, A1, A1
BGE A1, X0, loop
需留意組合語言語義與暫存器之間的方向性。與 GNU 是完全相反,所以已經建立起習慣的讀者在閱讀時需轉換一下。
這個 4 位元恰好可以形成一個 16 進位碼,所以只剩下最後一個判斷:要是這個值小於等於 9,那麼就應該用 0-9 的 ASCII 碼(48~57)輸出;反之,則應該用 A-F 的 ASCII 碼(87~92)輸出。由於 SBI 呼叫必然會摧毀 A0
、A6
、A7
,所以 A0
的輸入值應該要在一開始被保存起來:
TEXT dump(SB),NOSPLIT|NOFRAME,$0
+ MOV A0, A4
ADDI $15, X0, A1
loop:
+ // recover the input
+ MOV A4, A0
...
AND A3, A2, A0
+ // compare to 9
+ ADDI $9, X0, A2
+ BLT A2, A0, hexa
+hexn: // number
+ ADD $48, A0, A0
+ JMP hex
+hexa: // alphabet
+ ADD $87, A0, A0
+hex:
+ // print: A0 is already done
+ MOV $1, A7
+ MOV $0, A6
+ ECALL
// the end of loop
ADDI $-1, A1, A1
BGE A1, X0, loop
+ // newline
+ MOV $10, A0
+ MOV $1, A7
+ MOV $0, A6
+ ECALL
+
最後再搭配一個回傳(return)指令:
ECALL
+ RET
就能夠當作函式來呼叫了。
scause
那麼,會進到早期錯誤處理函式的原因究竟是什麼呢?這個問題的答案就在 scause
狀態暫存器之中。要直接觀察的話,我們在早期錯誤處理函式之中補上這一段:
ECALL
+ CSRRS CSR_SCAUSE, X0, A0
+ CALL dump(SB)
WFI
RISC-V 小常識:
CALL
和RET
實際上都是虛擬指令,對應到JALR
(或JAL
),也就是無條件跳躍指令。在呼叫時,下一行的位址會被作為回傳時應抵達的位址,存放在ra
暫存器之中;回傳時,則是會無條件跳躍至ra
暫存器內容之位址。
將 scause
狀態暫存器的內容讀取到 a0
之中,重編之後的執行結果:
...
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000a109
HI0000000000000005
我們拿到了一個例外代碼 5。之所以能夠斷言這是例外而非中斷,是因為 RISC-V 規格當中,以 *cause
狀態暫存器最高位元當作這個判斷,1 則為中斷,0 則為例外。
那麼 5 代表的又是什麼例外呢?這是記憶體讀取錯誤(Load Access Fault),可參考權限指令集規格書的列表 3.6。
stval
若是只知道例外的成因,也仍然不太方便除錯,因此有一部分的例外發生時,會設置相關的資訊在 stval
當中。此外,也因為這類的錯誤原本大多只有記憶體存取相關的錯誤,因此在舊版本的規格書中,這個狀態暫存器曾經被稱為 sbadaddr
。總之,我們這裡使用一樣的手法:
CSRRS CSR_SCAUSE, X0, A0
CALL dump(SB)
+ CSRRS CSR_STVAL, X0, A0
+ CALL dump(SB)
WFI
可以觀察到:
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000a109
HI0000000000000005
0000000080017ee0
0x80017ee0
?QEMU 的 RISC-V 通用裝置的記憶體從 0x80000000
開始,這個位置算起還不到 1MB,怎麼可能會有問題呢?當然會有的。
OpenSBI 是運行在機器模式的常駐韌體,它自己有自己的資料結構。要是作業系統模式的系統無視它,而將它的記憶體區段內容全數充公,那系統還怎麼分工明確地運行下去呢?所以 OpenSBI 使用物理記憶體保護(PMP)功能,解決這個問題。稍微往上巡視舊的控制台輸出內容,則可以看到
Domain0 HARTs : 0*,1*,2*,3*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
stval
這裡面的值 0x80017ee0
坐落在 Region00
裡面,沒有讀取(R)、寫入(W)和執行(X)之中的任何權限,所以若是對它讀取了,就會觸發讀取的錯誤,代碼 5 號。
sepc
可以存取 github 以進行以下實驗。
最後一個關鍵問題就是例外發生的地方了,記錄在 sepc
裡面。一樣:
CSRRS CSR_STVAL, X0, A0
CALL dump(SB)
+ CSRRS CSR_SEPC, X0, A0
+ CALL dump(SB)
WFI
執行結果為:
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000a109
HI0000000000000005
000000008001bee0
00000000802554cc
0x802554cc
是物理位址,日後(我們還沒啟用)對應到的虛擬記憶體位址是 0xffffff80000554cc
,這個位置是:
ffffff80000554b0 <_rt0_riscv64_opensbi>:
ffffff80000554b0: 0480051b addiw a0,zero,72
ffffff80000554b4: 0010089b addiw a7,zero,1
ffffff80000554b8: 0000081b sext.w a6,zero
ffffff80000554bc: 00000073 ecall
ffffff80000554c0: 00000517 auipc a0,0x0
ffffff80000554c4: fb050513 addi a0,a0,-80 # ffffff8000055470 <early_halt
>
ffffff80000554c8: 10551073 csrw stvec,a0
ffffff80000554cc: 00013503 ld a0,0(sp)
ffffff80000554d0: 00810593 addi a1,sp,8
ffffff80000554d4: 00000f97 auipc t6,0x0
ffffff80000554d8: 00cf8067 jr 12(t6) # ffffff80000554e0 <main>
ffffff80000554dc: 0000 unimp
事實上,就在我們設置 stvec
之後的第一行!這時候對 sp
暫存器所在的位址(應該就是前述的 0x8001bee0
),讀取一個值到 a0
暫存器,觸發了讀取存取例外。
予焦啦!今天看了很多組語,也實際上捲起袖子來寫組語程式,雖然都還很微小,但是已經可以透過這些簡單的方法做除錯以及蒐集線索。無論如何,我們明日再會!