iT邦幫忙

2021 iThome 鐵人賽

DAY 8
1
Software Development

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

予焦啦!使用 GDB 推進

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

予焦啦!回顧昨日,我們從 Linux 當中學習一些靈感,但我們的結論是現在什麼先暫時都不必做;不管是 CPU ID、裝置樹位址或是堆疊指標的指定。不如就先這樣下去,看看會發生什麼事吧。

本節重點概念

  • 基礎
    • GDB 常用指令
  • Golang
    • 執行期初始化問題排除範例
    • 組合語言內的結構偏移量運算子 _

關於 Golang 執行期初始化,可參閱拙作,雖然是以 x86_64 為主就是了。

試著走下去

我們已經很清楚,堆疊指標這時候還沒有完全搞定。是可以像 Linux 那樣,在虛擬記憶體啟用之前先設一個,啟用後再設一個,但何必這麼麻煩呢?我們先把它拿掉:

diff --git a/src/runtime/rt0_opensbi_riscv64.s b/src/runtime/rt0_opensbi_riscv64.s
index c65afa5c79..878b92b7bb 100644
--- a/src/runtime/rt0_opensbi_riscv64.s
+++ b/src/runtime/rt0_opensbi_riscv64.s
@@ -67,8 +67,6 @@ TEXT _rt0_riscv64_opensbi(SB),NOSPLIT|NOFRAME,$0
        ECALL
        MOV     $early_halt(SB), A0
        CSRRW   CSR_STVEC, A0, X0
-       MOV     0(X2), A0       // argc
-       ADD     $8, X2, A1      // argv
        JMP     main(SB)
        
        

然後執行看看。結果我們可以看到再次卡入早期錯誤處理,

...
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000a109
HI0000000000000007
000000008001ded8
0000000080252428

這次是 scause 為 7,代表記憶體寫入存取錯誤。原因也和之前一樣是誤觸 OpenSBI 的物理記憶體保護區段。而錯誤位置在:

ffffff8000052428 <runtime.rt0_go>:
ffffff8000052428:       fe113c23                sd      ra,-8(sp)

其實也還沒有多遠,就在我們從 _rt0_riscv64_opensbi 進入點跳往 main,再由 main 函式裡面跳往位在 src/runtime/asm_riscv64.srt0_go 的第一行。我們即將與 Linux 共用這個階段的初始化。但也一樣又存取到了堆疊指標。

但我們今天是打定主意來試試看多少往前走的,那麼就隨便設置一個無傷大雅的值好了。如先前的函式序幕(prologue)範例與這裡的範例,我們知道堆疊指標是隋著程式的進行由高位往低位發展的,所以不如就先設在 256MB 的位置吧,但別忘了基底是從 0x80000000 起算,也就是說改成:

...
        JMP     main(SB)
 
 TEXT main(SB),NOSPLIT|NOFRAME,$0
        MOV     $runtime·rt0_go(SB), T0
+       MOV     $0x90000000, X2
        JALR    ZERO, T0

然後試跑:

Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000a109
HI0000000000000005
74e0e55be0d63618
0000000080254510

又是記憶體讀取錯誤。而且錯誤的位置在一個很誇張的地方,完全無法對應到實體記憶體。發生錯誤的所在是:

ffffff80000544f0 <runtime.gcWriteBarrier>:
ffffff80000544f0:       f2113023                sd      ra,-224(sp)
ffffff80000544f4:       f2010113                addi    sp,sp,-224
ffffff80000544f8:       0ca13423                sd      a0,200(sp)
ffffff80000544fc:       0cb13823                sd      a1,208(sp)
ffffff8000054500:       030db503                ld      a0,48(s11)
ffffff8000054504:       0a053503                ld      a0,160(a0)
ffffff8000054508:       00001fb7                lui     t6,0x1
ffffff800005450c:       01f50fb3                add     t6,a0,t6
ffffff8000054510:       6c0fb583                ld      a1,1728(t6) # 16c0 <internal/cpu.process
Options-0xffffff8000000940>
...

這時候就不得不強調,讓十六進位的後五位數在兩種定址模式底下維持一樣可以讓問題輕鬆許多。此時的連結位址是 0xffffff8000054510 而載入位址是 0x80254510,算是很方便可以定位的了。

筆者當然也是可以就自己淺薄的了解,就地開始解釋 gcWriteBarrier 函式的任務是什麼,但這樣有點見樹不見林。我們現在有一個應該還堪用的堆疊指標,所以 Golang 執行期也就這麼執行下去,可是依循了怎麼樣的軌跡,導致程式最後死在這裡呢?又,有沒有可能其實我們從早期錯誤處理觀察到的錯誤狀態,實際上已經是第二、第三現場,距離案發地點已經有一段距離了呢

有時候,系統的問題就是需要抽絲剝繭、從各個角度去觀察才有辦法慢慢掌握問題,這時候,只有印出訊息功能來輔佐除錯的話,有時候還是過於虛弱。所以我們就趁這個機會,來使用最權威的除錯工具:GDB。

GDB 登場!

GDB 是 GNU Project Debugger,它有著強大的功能。筆者這裡只打算介紹一些簡單的常用指令,並展示它們已經足夠強大,來輔助我們的除錯。

QEMU 模擬器有提供機制讓 GDB 能夠連接除錯。我們首先打起 QEMU,但多附帶 -S -s。其中,-S 代表在開始的時候凍結模擬器的執行,-s 代表預設將除錯埠(port)設置在本機的 1234。

qemu-system-riscv64 \
        -smp 4 \
        -M virt \
        -m 256M \
        -nographic \
        -bios misc/opensbi/build/platform/generic/firmware/fw_jump.bin \
        -device loader,file=ethanol/goto/goto.bin,addr=0x80200000 \
        -device loader,file=ethanol/ethanol,addr=0x80201000,force-raw=on -S -s
        

這麼執行之後,有別於先前都會快速進入 OpenSBI,現在就是凍結的狀態了。在另外一個終端機,我們可以使用針對 RISC-V 的 GDB 來連線:

筆者在 Arch Linux 直接安裝了 community/riscv64-linux-gnu-gdb 來使用。當然,可以自己建置或是使用預先編好的版本。

$ riscv64-linux-gnu-gdb -ex "target remote :1234"                                        [6/346]
GNU gdb (GDB) 10.2                                                                              
Copyright (C) 2021 Free Software Foundation, Inc.               
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.   
There is NO WARRANTY, to the extent permitted by law.               
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x0000000000001000 in ?? ()
(gdb) 

最一開始,QEMU 有一小段啟動程式碼,位在 0x1000 的區域,可以使用 x/10i $pc 指令來觀察當前程式指標($pc)起算十行組合語言的內容:

(gdb) x/10i $pc
=> 0x1000:      auipc   t0,0x0
   0x1004:      addi    a2,t0,40
   0x1008:      csrr    a0,mhartid
   0x100c:      ld      a1,32(t0)
   0x1010:      ld      t0,24(t0)
   0x1014:      jr      t0
   0x1018:      unimp
   0x101a:      0x8000
   0x101c:      unimp
   ...

請留意,由於這是 GNU 工具,所以組合語言的目的、來源方向也是相反於 Golang 組語。靠近助憶符的是目標(被改寫的對象),而較遠的通常是來源運算元。

這裡有四個暫存器被寫入。a0~2 等三個暫存器是作為系統軟體之間的參數,所以我們目睹到了 a0 被設為 Hart ID 的現場。a1 也即將被設為 DTB 的所在。至於 a2,不在這次系列文的範疇,但有興趣的讀者可以去搜尋 OpenSBI 動態模式(dynamic mode)。

最後在 0x1014 之處,跳向 t0。在這之前,t0 暫存器歷經一個 auipc 指令,以及一個 ld 指令。

前者是指加入常數到程式指標的高位(除去低 12 bit)部分,因此執行完 0x1000 指令時,t0 應該已經成為 0x1000,畢竟該指令附帶的常數為 0x0。這個可以使用以下 GDB 指令序列驗證:

(gdb) p/x $t0
$1 = 0x0
(gdb) si
0x0000000000001004 in ?? ()
(gdb) p/x $t0
$2 = 0x1000
(gdb)

筆者這裡先印出 t0 暫存器的值,之後使用 si(step instruction,執行單一一行指令),程式指標轉移到下一行,且 0x1000 的指令生效,t0 被寫入 0x1000

後者,也就是一個讀取指令,是以 t0 為基底取 24 位元組的位置(整組 24(t0) 即為此意),將這個位置的內容存放回 t0 暫存器。當然我們可以使用多個 si 指令推移程式行進,但我們也可以使用強大的設置斷點功能,直接先守在 0x1014 處,跳躍之前:

(gdb) p/x $t0
$2 = 0x1000
(gdb) b *0x1014
Breakpoint 1 at 0x1014
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, 0x0000000000001014 in ?? ()
(gdb) p/x $t0
$3 = 0x80000000
(gdb)

所以我們知道了,稍後程式執行的跳轉目的地是 0x80000000,也就是 OpenSBI,或者說 QEMU 參數 -bios 指向的位址。

在先前上傳的 Hoddara repo 裡面,其實沒有 -bios 參數帶入給 QEMU,而是使用內建的 OpenSBI 韌體。QEMU 在啟用 RISC-V 平臺時,要是沒有 -bios 參數,就會自動使用之。這裡我們為了 GDB 的使用順暢,暫且跳過 OpenSBI 的準備部分,留到本日最後的一小節。

OpenSBI

往前進一行,我們就能來到 OpenSBI 的領域。

(gdb) si
0x0000000080000000 in ?? ()
(gdb) x/10i $pc
=> 0x80000000:  add     s0,a0,zero
   0x80000004:  add     s1,a1,zero
   0x80000008:  add     s2,a2,zero
   0x8000000c:  jal     ra,0x80000698
   0x80000010:  add     a6,a0,zero
   0x80000014:  add     a0,s0,zero
   0x80000018:  add     a1,s1,zero
   0x8000001c:  add     a2,s2,zero
   0x80000020:  li      a7,-1
   0x80000022:  beq     a6,a7,0x8000002a
(gdb)

看起來確實像是有東西,而不是像先前 0x1000 的小型啟動碼數行之後就會開始出現 unimp 這種未定義內容。但這裡顯示的都是十六進位數字,比起純然的二進位檔也只多了一點點可讀性而已。當然,GDB 不會讓開發者失望的。

編譯的產物如果夾帶有除錯訊息,GDB 就可以提取出來參照使用。所以這裡我們可以使用 add-symbol-file 指令:

(gdb) add-symbol-file misc/opensbi/build/platform/generic/firmware/fw_jump.elf           [0/399]
add symbol table from file "misc/opensbi/build/platform/generic/firmware/fw_jump.elf"
(y or n) y
Reading symbols from misc/opensbi/build/platform/generic/firmware/fw_jump.elf...
(gdb) x/10i $pc
=> 0x80000000 <_start>: add     s0,a0,zero
   0x80000004 <_start+4>:       add     s1,a1,zero
   0x80000008 <_start+8>:       add     s2,a2,zero
   0x8000000c <_start+12>:      jal     ra,0x80000698 <fw_boot_hart>
   0x80000010 <_start+16>:      add     a6,a0,zero
   0x80000014 <_start+20>:      add     a0,s0,zero
   0x80000018 <_start+24>:      add     a1,s1,zero
   0x8000001c <_start+28>:      add     a2,s2,zero
   0x80000020 <_start+32>:      li      a7,-1
   0x80000022 <_start+34>:      beq     a6,a7,0x8000002a <_try_lottery>

一樣的位址,稍微加了一點料的輸出,但我們現在可以看到位址與符號(symbol)的對應關係了。口說無憑,我們可以在 OpenSBI 的程式碼中搜尋這些符號,且看合不合理:

$ grep 'fw_boot_hart' -R ./
./firmware/fw_payload.S:        .global fw_boot_hart
./firmware/fw_payload.S:fw_boot_hart:                                                           
./firmware/fw_dynamic.S:        .global fw_boot_hart
./firmware/fw_dynamic.S:fw_boot_hart:
./firmware/fw_base.S:   call    fw_boot_hart
./firmware/fw_jump.S:   .global fw_boot_hart
...

從除錯器看到的是跳躍到 fw_boot_hart,而搜出來的四個檔案只有一個是呼叫,所以先觀察 firmware/fw_base.S

...
_start:
        /* Find preferred boot HART id */
        MOV_3R  s0, a0, s1, a1, s2, a2
        call    fw_boot_hart
        add     a6, a0, zero
        MOV_3R  a0, s0, a1, s1, a2, s2
        li      a7, -1
        beq     a6, a7, _try_lottery
...

正是如出一轍。

推進到 ethanol

但我們無需在這裡久留。再度設置斷點於 0x80200000 的 goto 小程式所在位址,然後前進:

(gdb) x/10i $pc                                  
=> 0x80200000:  auipc   t6,0x55
   0x80200004:  addi    t6,t6,1200
   0x80200008:  jr      t6
...
(gdb) si
0x0000000080200004 in ?? ()
(gdb) 
0x0000000080200008 in ?? ()
(gdb) 
0x00000000802554b0 in ?? ()

如果不輸入指令,單純按輸入鍵的話,就會有重複上一個指令的效果。

來到實體位置 0x802554b0,正是這個版本的 ethanol 映像檔的進入點位址對應的實體位址:

Entry point address:               0xffffff80000554b0

但由於連結位址(link address)與載入位址(load address)不相同,所以就算如同剛才 OpenSBI 那樣,引用 add-symbol-file,再透過 GDB 觀察程式碼也是沒有效果的:

(gdb) add-symbol-file ethanol/ethanol
...
(gdb) x/10i $pc
=> 0x802554b0:  addiw   a0,zero,72
   0x802554b4:  addiw   a7,zero,1
   0x802554b8:  sext.w  a6,zero
   0x802554bc:  ecall
   0x802554c0:  auipc   a0,0x0
   0x802554c4:  addi    a0,a0,-80
   0x802554c8:  csrw    stvec,a0
   0x802554cc:  auipc   t6,0x0
   0x802554d0:  jr      12(t6)

這時候我們仍然可以強制指定,我們希望程式碼區段的符號能夠做特定的位址對應,指令如下:

(gdb) add-symbol-file ethanol/ethanol -s .text 0x80202000             
add symbol table from file "ethanol/ethanol" at                                                      
        .text_addr = 0x80202000                                                                 
(y or n) y                                                                                      
Reading symbols from ethanol/ethanol...
...
(gdb) x/10i $pc                                  
=> 0x802554b0 <_rt0_riscv64_opensbi>:   addiw   a0,zero,72
   0x802554b4 <_rt0_riscv64_opensbi+4>: addiw   a7,zero,1
   0x802554b8 <_rt0_riscv64_opensbi+8>: sext.w  a6,zero
   0x802554bc <_rt0_riscv64_opensbi+12>:        ecall
   0x802554c0 <_rt0_riscv64_opensbi+16>:        auipc   a0,0x0
   0x802554c4 <_rt0_riscv64_opensbi+20>:        addi    a0,a0,-80
   0x802554c8 <_rt0_riscv64_opensbi+24>:        csrw    stvec,a0
   0x802554cc <_rt0_riscv64_opensbi+28>:        auipc   t6,0x0
   0x802554d0 <_rt0_riscv64_opensbi+32>:        jr      12(t6)
...

筆者也不清楚為什麼 early_haltmain 這裡沒有被 GDB 偵測出來。要詳細理解的話,可能必須深入挖掘 GDB 與映像檔的除錯資訊本身,這就超過本系列範圍了。

追蹤錯誤

我們接下來在最後回報的錯誤位址設下斷點,

(gdb) b *0x80254510
Breakpoint 1 at 0x80254510
(gdb) c
Continuing.
...
(gdb) x/10i $pc-0x10
   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

果然沒錯,就是這裡,但是這時候就已經達成錯誤條件了嗎?事實上,各位讀者或許都曾耳聞 Golang 的垃圾回收(Garbage Collection)機制做得很好,其中這個 gcWriteBarrier 也是關鍵程序之一,它在許多地方都會被執行到。

為此,我們檢驗一下這時的 t6 暫存器,畢竟是準備要拿來存取的記憶體基底:

(gdb) p/x $t6
$1 = 0x74e0e55be0d62f59
(gdb) si
...
early_halt () at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/rt0_opensbi_riscv64.s:61
61              JMP     early_halt(SB)
(gdb)

這時候的 t6 就已經是一個誇張的內容了,所以存取之後當然會陷入我們的早期錯誤處理區,這也是符合預期的。

追溯原因

如果只是在這裡觀察 t6 且確認其內容錯誤,本身實在沒什麼了不起。所以我們再開一次 GDB,停在觸發錯誤的前一刻:

(gdb) bt
#0  0x0000000080254510 in runtime.gcWriteBarrier ()
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:653
#1  0x000000008023ebd8 in runtime.args (c=-2145037200, v=0x82200000)
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/runtime1.go:63
#2  0x00000000802524b4 in runtime.rt0_go ()
    at /home/noner/FOSS/hoddarla/ithome/go/src/runtime/asm_riscv64.s:54

bt 指令是向後追蹤(backtrace)的簡寫,是在堆疊當中搜尋呼叫歷史的強大功能。我們這裡就追溯到了,從 rt0_go 進入之後的這個 args,再呼叫了 gcWriteBarrier,顯然造成了問題。

分析

再看一次案發現場:

   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

以及它對應的組合語言原始碼,在 src/runtime/asm_riscv64.s

647 TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$2216   ...
651         MOV     g_m(g), A0 
652         MOV     m_p(A0), A0
653         MOV     (p_wbBuf+wbBuf_next)(A0), A1
...

這裡必須解釋一下 Golang 的組合語言檔案中,g_m(g) 或是 m_p(A0) 作為一種來源運算元的語義。

Golang 執行期有些重要的結構體,如 g,定義在 src/runtime/runtime2.go

 403 type g struct {
 ...
 417         m         *m 
 418         sched     gobuf
 419         syscallsp uintptr
 420         syscallpc uintptr 
 ...

其中有個 m 成員,型別為 *m,也就是 m struct 的指標。這些都是沒什麼大不了的程式語言語法。但在組合語言裡面存取的時候,畢竟就不像高階語言一樣可以方便地使用 g_instance_x.m 這樣的方式去存取一個結構裡面的成員變數了。

所以,g_m(g) 代表的是,對於括號內的暫存器(這裡是 g,通用暫存器 s11 在 Golang 裡面的別名),將它當作一個 g struct (底線之前為結構名稱)的位址,然後取得 m 這個成員的位移量。以這個例子來說,反應在 Golang 與 GNU 兩種風格的組合語言就是

   MOV     g_m(g), A0
   
   就是
   
   0x80254500:      ld      a0,48(s11)

我們可以如何印證這件事情?g 結構內的 m 成員的偏移量是 48 個位元組,這個可以透過靜態分析觀察 g 的內部結構,再加以計算。事實上,在 m 成員之前有 6 個指標,因此 m 的偏移量是 6*8 = 48 位元組,也就不足為奇。

也就是說,Golang 組合語言裡面的底線,等同於在 .go 檔當中的 . 存取成員運算子的弱化版:它是一個偏移量,需要搭配存放在暫存器中的記憶體位址當作基底。

回顧產生問題的部分,從 objdump 工具或是 gdb 反組譯,看到的是

   0x80254500 <runtime.gcWriteBarrier+16>:      ld      a0,48(s11)
   0x80254504 <runtime.gcWriteBarrier+20>:      ld      a0,160(a0)
   0x80254508 <runtime.gcWriteBarrier+24>:      lui     t6,0x1
   0x8025450c <runtime.gcWriteBarrier+28>:      add     t6,a0,t6
=> 0x80254510 <runtime.gcWriteBarrier+32>:      ld      a1,1728(t6)

這部分的語義細節非常少,只看到一些加加減減的運算。所以還是看原始檔:

647 TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$2216   ...        
651         MOV     g_m(g), A0 
652         MOV     m_p(A0), A0
653         MOV     (p_wbBuf+wbBuf_next)(A0), A1
...

我們就更能夠理解這一段真正的意義所在。首先是從 g 暫存器存取 m,然後是從這個 m 取得 p,然後再取得兩個偏移量一口氣加總起來從這個 p 的基底開始算,最後在真正能夠把該記憶體的內容存放在 A1 之前,就已經發生了錯誤。

使用 GDB 彌補靜態分析的弱點

以上都是靜態分析,光看程式碼就能夠理解的,但這沒有辦法幫助我們理解問題的成因。我們需要一些現場的照片和線索,才有辦法進行推理。這就是 GDB 的強項了。

首先,先窺探一下 s11,也就是 Golang 這邊稱呼的 g。其實 g 是 Golang 執行期模型裡面很重要的一個角色:Goroutine。它是比執行緒(thread)更輕量的共常式(coroutine),完全由使用者空間的執行期控制。但我們這些先無需理解細節,只要知道在絕大部分的時間點,一支 RISC-V 上的 Golang 程式的 s11 或是 g 暫存器裡面存放的即是當前所屬的 Goroutine 即可。

對於 Golang 的 G-M-P 模型感興趣的讀者可以參考拙作,雖然只是一篇學習筆記,但有附帶一些更詳盡的說明文章。

(gdb) p/x $s11
$3 = 0x802abea0
(gdb) x/10gx 0x802abea0
0x802abea0 <runtime.g0>:        0x000000008ffeffe0      0x000000008fffffe0
0x802abeb0 <runtime.g0+16>:     0x000000008fff0380      0x000000008fff0380
0x802abec0 <runtime.g0+32>:     0x0000000000000000      0x0000000000000000
0x802abed0 <runtime.g0+48>:     0x00000000802ac040      0x0000000000000000
0x802abee0 <runtime.g0+64>:     0x0000000000000000      0x0000000000000000

經過 GDB 的符號解析,我們知道這時候的 s11 ,儘管仍在非常早期的階段,已經設置為 g0 這個 Golang 預設的 Goroutine 了,而不是某個在執行的後期才動態配置出來的 Goroutine。

s11 為基底,48 為偏移量,取得的值是 0x802ac040,所以這就是那個 m

x/10gx 0x00000000802ac040
0x802ac040 <runtime.m0>:        0x00000000802abea0      0x34e852b3f52eb1f9
0x802ac050 <runtime.m0+16>:     0x97d2168c4acd91e9      0x18afc19f7bb6f487
0x802ac060 <runtime.m0+32>:     0xf35dac95931c6ccc      0x39cfba9e1ee9dd06
0x802ac070 <runtime.m0+48>:     0xa306e80df3b88efc      0x68c2b9fd68b8349d
0x802ac080 <runtime.m0+64>:     0xc1e6df31a41e9370      0x001d90f047b05275
...
0x802ac0d0 <runtime.m0+144>:    0x04a8fd952fb5acef      0x69821f9b0ef12226
0x802ac0e0 <runtime.m0+160>:    0x74e0e55be0d61f59      0xa344a5670bbf4628
0x802ac0f0 <runtime.m0+176>:    0xad5cad7fddacc029      0x17088f3812d6f3cc

這個 m0 也是一個預先定義好的變數,兩者同在 src/runtime/proc.go 之中被定義。偏移量 160 的內容是 0x74e0e55be0d61f59 當然看起來就不是一個很正常的位址,最後的錯誤也發生在這裡:這個位置被當作 P 結構的基底去存取了。

話說這個 src/runtime/proc.go 檔案的一開頭也有非常詳盡的 G-M-P 模型描述,事實上,是整個 Golang 的排程器(scheduler)的描述。

但如果 m0 的內容本來就包含這些亂七八糟的值,那我們又有什麼方法可以避免這個錯誤呢?

附錄:建置 OpenSBI

若想略過以下步驟,也可以直接使用今天更新的 Hoddarla repomake opensbi 指令。

取得 OpenSBI 原始碼

有兩種方法,一種是取得 tar 原始檔案集(tarball),位在這裡;或是直接使用 git 指令取得最新的開發版本,

git clone https://github.com/riscv-software-src/opensbi

編譯

筆者假設各位有興趣的讀者都已經在先前的章節中,曾經以 . .hdlarc 指令將 RISC-V 的交叉工具鏈放置在可執行的路徑環境變數之中。

  1. 進入原始碼資料夾中。
  2. CROSS_COMPILE=riscv64-buildroot-linux-musl- make PLATFORM=generic

生成的 build/platform/generic/firmware/fw_jump.bin 檔案,即是本日實驗當中用來當作 -bios 參數傳入的檔案囉!

小結

予焦啦!今日在這裡戛然而止。分明才剛開始的旅程,為什麼看起來已經撞上了一堵高牆呢?理所當然的存取,按著記憶體基底與偏移量取值,只是原先那些結構體裡面的值偏偏就是會造成存取錯誤的值。

先給各位讀者一個關鍵字:.bss。我們明日再會!


上一篇
予焦啦!前期分析
下一篇
予焦啦!BSS 初始化
系列文
予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索33

尚未有邦友留言

立即登入留言