iT邦幫忙

2021 iThome 鐵人賽

DAY 6
2
Software Development

予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索系列 第 6

予焦啦!使用暫存器除錯

本節是以 Golang 上游 ee91bb83198f61aa8f26c3100ca7558d302c0a98 為基準做的實驗。

予焦啦!回顧第零章,我們有了一支可以充分支援開發的工具鏈,也建立了模擬器的使用經驗。開機啟動流程上了軌道,我們已經確實進入到 ethanol 的映像檔了。並且,就在昨日,我們導入早期錯誤處理用的微程式,成功捕捉第一個未知的錯誤。

然而,在真正開始開發任何認真的功能之前,為了避免除錯所需的時間耗損,我們還是先準備一些除錯工具和技能吧!

本節重點概念

  • RISC-V
    • 簡單的組合語言設計
    • 控制暫存器
      • scause
      • sepc
      • stval
    • 例外:存取錯誤

前置準備

仔細想想,到昨日為止,我們成功完成的事情也就只是卡在 ethanol 核心最一開始的地步。雖然卡住了,但也沒有辦法回答這些問題:

  • 是因爲什麼錯誤而觸發 early_halt
  • 在哪裡?
  • 錯誤的內容是什麼?

但有一就有二,我們能印出一個字元,理論上就能夠印出更多,提供更全面的訊息。以上三個問題,都是很理所當然的基本問題,RISC-V 當然也是支援的。事實上,以上三個問題的解答,可以分別從三個狀態暫存器取得:scausesepc 以及 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 或是其他 Print 函式?一來 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 呼叫必然會摧毀 A0A6A7,所以 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 小常識:CALLRET 實際上都是虛擬指令,對應到 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 暫存器,觸發了讀取存取例外。

小結

予焦啦!今天看了很多組語,也實際上捲起袖子來寫組語程式,雖然都還很微小,但是已經可以透過這些簡單的方法做除錯以及蒐集線索。無論如何,我們明日再會!


上一篇
予焦啦!支援 RISC-V 權限指令與暫存器
下一篇
予焦啦!前期分析
系列文
予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索32

尚未有邦友留言

立即登入留言