iT邦幫忙

2021 iThome 鐵人賽

DAY 4
2

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

予焦啦!回顧昨日,最終我們能夠透過

$ GOOS=opensbi GOARCH=riscv64 go build ../../ethanol/ethanol.go

這般非常相似於一般 Golang 開發應用程式的用法,產生一個還只是空殼的 opensbi/riscv64 系統組合的可執行檔。

接下來的目標是,確認這個剛編出來的東西真的能夠被當作一個作業系統映像檔嗎?它如何能夠作為 RISC-V 系統開機的一部分?筆者也會在今日檢驗的過程中回顧一些相關的軟體。

本節重點概念

  • 基礎
    • 編譯流程
    • ELF 檔基本結構
    • QEMU RISC-V Generic 裝置的啟動流程
  • Golang
    • 連結器參數 -T-R 的控制
    • 組合語言基礎
  • RISC-V
    • ecall 指令
    • 例外處理簡述

ELF 檔案檢驗

我們可以透過 GNU binutils 工具包當中的 readelf 工具觀察這個產出的可執行檔:

$ riscv64-buildroot-linux-musl-readelf -h hw        
ELF Header:                                     
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64     
  Data:                              2's complement, little endian
  Version:                           1 (current) 
  OS/ABI:                            UNIX - System V
  ABI Version:                       0                                                          
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x64418
  ...

readelf 認識它,就表示它至少還算是個正常可辨識的 ELF 檔。然而看到這個進入點位址(Entry point address),就有點不太對了。

以作業系統的慣例來講,核心的可執行區段位置通常是位在所有可用的虛擬空間的最高位的部分。若是 64 位元系統,以 RISC-V Linux 的 sv39 組態(將於日後介紹虛擬記憶體的篇章詳談)來講的話,核心的位置會從 0xffffffe000000000 起始,可參考arch/riscv/Kconfig

有一個因素是,核心就可以簡單地將使用者程式配置在低位,且可以一路往高位延展。至於具體來說為什麼這會成為一種慣例,我認為這篇是很值得參考的資料。

話說回來,這裡看到的進入點位置是 0x64418,當然是非常低的。若使用 readelf 工具近一步檢驗各個 ELF 區段的位置,也可以發現它們都在很低的位置。這也當然,畢竟我們並沒有特別針對這個系統組合做些甚麼特別的處理。RISC-V Linux 中具體的參照,在 arch/riscv/kernel/vmlinux.ld.S 檔案當中,Linux 便會指定連結器將程式碼區段從前段中提及的高位置開始擺放。

那麼,該怎麼樣修改?Golang 是否也有 C 語言生態系裡面的連結器腳本(linker script)之類的文件,可以供開發者手動調整可執行檔中的區段位址呢?在這之前,我們先繼續引用原本的 readelf 工具觀察,這個預設的區段排列長成什麼樣子:

$ riscv64-buildroot-linux-musl-readelf -S hw                        
There are 23 section headers, starting at offset 0x158:             
                                                                                                
Section Headers:                                                                                
  [Nr] Name              Type             Address           Offset  
       Size              EntSize          Flags  Link  Info  Align  
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0   
  [ 1] .text             PROGBITS         0000000000011000  00001000  
       000000000005405c  0000000000000000  AX       0     0     8      
  [ 2] .rodata           PROGBITS         0000000000070000  00060000
       000000000001e8d5  0000000000000000   A       0     0     32  
  [ 3] .shstrtab         STRTAB           0000000000000000  0007e8e0
       000000000000017a  0000000000000000           0     0     1
...

第零個區段必須是空的,似乎是連結器背後的一些歷史因素導致的。

因此關鍵就在於回答這兩個問題:Golang 開發者能夠更動 .text 區段的開頭,從 0x11000 變成某個看起來更氣派的高位址嗎?如何做到?

檢視編譯流程

回顧 C 語言流程

比較粗略一點講的話,大家都稱呼 gcc 為編譯器(compiler),因為它能夠將人們餵給它的可讀的程式碼轉換成可執行檔。但嚴謹來講,它只是一個編譯驅動(compiler driver),負責把整個流程管理好。所謂整個流程,是因為其中牽涉到的元件至少有以下數個:

  1. 前置處理器(preprocessor):從一般 C 程式碼到展開完全的 C 程式碼
  2. 編譯器:將 C 程式碼轉換成組合語言
  3. 組譯器(assembler):將組合語言轉換為物件檔
  4. 連結器(linker):將單一或多個物件檔合併(因為需要定位多個物件之間的參照關係)轉換為可執行檔

其中,若要調整程式碼區段,那麼開發者必須調整第 4 個步驟中,多餵一個連結器腳本,去指定區段的位址甚至符號(變數、函數等等)的位址與對齊關係(alignment,比方說有些地方開發者希望可以 8 個位元組為單位排列所有內容,有些地方 2 個位元組為單位即可)。

Golang 流程

那麼以 Golang 來說,我們目前為止相當於只觀測到 go build 作為編譯驅動器的功能。只要能夠先特定出連結器的階段,或許就能夠掌握到線索了。以這個為契機,還是好好閱讀一下文件吧:

$ go build help
...
非常簡潔的描述 build 的行為,值得一看。
...
        -a
                force rebuilding of packages that are already up-to-date.
                譯:即使已經存在,也強迫重新編譯。
        -x
                print the commands.
                譯:印出執行的指令。
        -work
                print the name of the temporary work directory and
                do not delete it when exiting.
                譯:印出暫存的工作目錄,並保存所有中間產物。
...
        -asmflags '[pattern=]arg list'
                arguments to pass on each go tool asm invocation.
                譯:要餵給 Golang 工具 asm 的參數。
        -gcflags '[pattern=]arg list'
                arguments to pass on each go tool compile invocation.
                譯:要餵給 Golang 工具 compile 的參數。
        -ldflags '[pattern=]arg list'
                arguments to pass on each go tool link invocation.
                譯:要餵給 Golang 工具 link 的參數。

前半節的指令說明可以幫助我們一窺 go build 作為編譯驅動器展開整體流程之後的結果。有興趣的讀者不妨直接執行 go build -work -a -x 試試!可以觀測到每一個組件(像是昨日看到的 runtimeos 等等)先被個別編譯之後,最後才呼叫 link 工具產出執行檔:

...
/home/noner/FOSS/hoddarla/ithome/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=s6qzkJHXEbb67EhxxAGl/2SEhUWfSojBvAPlQVFt-/VZzZk01lY2DDxHbprbWq/s6qzkJHXEbb67EhxxAGl -extld=gcc $WORK/b001/_pkg_.a
/home/noner/FOSS/hoddarla/ithome/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal
cp $WORK/b001/exe/a.out hw

其中並沒有類似 C 語言裡面的連結器腳本。importcfg.link 看起來很可疑,但內容也僅是先前的編譯產物而已。但我們從文件當中可以得到別的靈感,也就是對應到 C 語言的編譯流程的每個階段(除了前置處理),都有可以額外傳入參數的部分。所以我們可以鎖定連結器,閱讀它的文件,赫然可見:

  -T address
        set text segment address (default -1)

所以 -T 看起來就是我們要的東西了!但一如往常的事情不會那麼順利,比方說筆者做的以下兩個實驗都吃鱉:

  1. 使用 -T 0xffffff8000000000 企圖一步到位,結果會回報編譯錯誤
$ GOOS=opensbi GOARCH=riscv64 go build -ldflags='-T 0xffffff8000000000' ethanol/ethanol.go
# command-line-arguments              
invalid value "0xffffff8000000000" for flag -T: value out of range
  1. 使用 -T 0x12000 小幅調整,結果雖然可以編譯出產物,看起來卻是壞掉的 ELF
$ riscv64-buildroot-linux-musl-objdump -d hw
riscv64-buildroot-linux-musl-objdump: hw: file format not recognized

Golang 連結器參數 -T-R

所以,深入瞭解 -T 參數是勢在必行了。我們可以開啟 src/cmd/link/internal/ld/main.go 檔案,並且在全域變數中找到 -T 的定義:

FlagRound         = flag.Int("R", -1, "set address rounding `quantum`")
FlagTextAddr      = flag.Int64("T", -1, "set text segment `address`")

這個東西,定義成 Int64 型別的話,那當然 0xffffff8000000000 的賦值就不會成功了,畢竟首位元為 1,這個值就是無號整數才能夠容納的了。這是第一個實驗失敗的原因,可以理解。如果要騙過它去真的使用這個高位位址,還是可以手動轉換 2 的補數,也就是 -T -0x8000000000,結果編譯可以成功,但 ELF 本身仍然有點毀損

$ riscv64-buildroot-linux-musl-readelf -h hw
ELF Header:                                     
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64     
  Data:                              2's complement, little endian
  Version:                           1 (current) 
  OS/ABI:                            UNIX - System V             
  ABI Version:                       0
  Type:                              EXEC (Executable file)              
  Machine:                           RISC-V                                                     
  Version:                           0x1
  Entry point address:               0xffffff8000053418
  Start of program headers:          64 (bytes into file)
  Start of section headers:          344 (bytes into file)
  Flags:                             0x4, double-float ABI
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         23
  Section header string table index: 3
readelf: Error: the PHDR segment is not covered by a LOAD segment

筆者找了一段時間,才鎖定實驗一與 ELF 毀損的現象,原因可能出在 -R 這個連結器參數上。若我們檢索昨日的改動,可以看到在 src/cmd/link/internal/riscv64/obj.go 檔案的 archinit 函式裡,初始化了 -T-R 分別對應到的參數:

func archinit(ctxt *ld.Link) {
        switch ctxt.HeadType {
                case objabi.Hlinux, objabi.Hopensbi:
                ld.Elfinit(ctxt)
                ld.HEADR = ld.ELFRESERVE
                if *ld.FlagTextAddr == -1 {
                        *ld.FlagTextAddr = 0x10000 + int64(ld.HEADR)
                }
                if *ld.FlagRound == -1 {
                        *ld.FlagRound = 0x10000
                }
...

這就解釋了為什麼預設的程式碼區段會從 0x11000 開始:因為通常 ELF 檔頭的大小(ld.ELFRESERVEld.HEADER)是 0x1000。Round 在這裡是進位的基準,設置成0x10000 代表有一些以這個值為單位的運算。這可以解釋為什麼單純設置 -T 0x12000 不成功,因為扣除 ELF 檔頭之後,程式碼區段的位址變成 0x11000 開始,但連結器針對 ELF 內其他部分的位址計算就因此沒有辦法整除 0x10000 而導致問題了。

所以,這個階段,我們可以先採用 -R 0x1000 -T -0x7ffffff000,來讓這個可執行檔的程式碼區段從 0xffffff8000001000 開始,並且也不會弄亂對齊:

$ riscv64-buildroot-linux-musl-readelf -l hw                           
Elf file type is EXEC (Executable file)
Entry point 0xffffff8000054418
There are 5 program headers, starting at offset 64
                                                
Program Headers:     
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0xffffff8000000040 0xffffff8000000040
                 0x0000000000000118 0x0000000000000118  R      0x1000
  NOTE           0x0000000000000f9c 0xffffff8000000f9c 0xffffff8000000f9c
                 0x0000000000000064 0x0000000000000064  R      0x4
  LOAD           0x0000000000000000 0xffffff8000000000 0xffffff8000000000
                 0x000000000005505c 0x000000000005505c  R E    0x1000
  LOAD           0x0000000000056000 0xffffff8000056000 0xffffff8000056000
                 0x0000000000051e18 0x0000000000051e18  R      0x1000
  LOAD           0x00000000000a8000 0xffffff80000a8000 0xffffff80000a8000
                 0x0000000000002c60 0x0000000000032280  RW     0x1000
...

其實,在 ELF 的術語裡面,有分 section 和 segment,前者是給連結器在連結期使用、後者則是載入器(loader)在載入期使用的。顯然這裡的對齊,是針對後者。可參考筆者在先前鐵人賽的拙作

很抱歉這裡兩者中文直譯可能都會有「區段」的困擾性。也有些簡體書籍應該是以區塊和區段作為區分手法,但仍然拙劣。只能慨嘆如今人們的翻譯能力完全不如 19 世紀的日本漢學家翻譯出「社會」、「經濟」、「政治」等造語。資訊時代的漢語智能,幾乎可以說是流失殆盡的。

試跑前的分析

誠然,我們目前唯一的核心檔案 ethanol/ethanol.go 是一個最基本的 Hello World,但筆者並沒有天真到認為這個執行檔能夠渡過 Golang 設計給使用者空間執行期初始化並最後引用到 fmt 組件,並成功呼叫 fmt.Println 函式,過程中會發生什麼問題還很難說。我們現在能夠比較確定的東西,其實只有整個可執行檔的進入點位在 _rt0_riscv64_opensbi 而已。

因此我們在這裡(src/runtime/rt0_opensbi_riscv64.s)插入一些程式碼吧!像是學 python 的時候插 print 函式來學習最基本的追蹤程式碼方法一樣,我們這裡加入:

 TEXT _rt0_riscv64_opensbi(SB),NOSPLIT|NOFRAME,$0
+       MOV     $0x48, A0
+       MOV     $1, A7
+       MOV     $0, A6
+       ECALL
        MOV     0(X2), A0       // argc
        ADD     $8, X2, A1      // argv
        JMP     main(SB)

這區區 4 行其貌不揚的組合語言,何以能夠支援印出訊息這樣複雜的功能?我們必須先瞭解以下概念:

  1. Golang 的組合語言(.s 檔)與 GNU 工具預設的組合語言語法不一樣,最大的差異在於順序。這段組合語言碼編譯完成後使用 objdump 反組譯的結果為:
ffffff8000054418:       0480051b                addiw   a0,zero,72
ffffff800005441c:       0010089b                addiw   a7,zero,1
ffffff8000054420:       0000081b                sext.w  a6,zero
ffffff8000054424:       00000073                ecall

a0a6a7 在這裡是暫存器,當然是運算的目標對象(destination),一開始的三行都只有賦值的效果,所以只有常數(immediate)作為運算的來源(source)的模式,但方向截然不同:Golang 是目標在後,整行的語義感覺像是從左到右延展,反過來 GNU 則是預設從右到左進行。當然,我們之後還會看到更複雜的組合語言指令,包含三個運算元(operand)也是常有的,屆時的「方向性」可能就不是單純賦值這麼直接,但大致上目標運算元的位置決定了兩者間最大的差異。又,MOV 並不是一個 RISC-V 真正定義的組合語言助憶符(mnemonic),而是 Golang 提供的跨架構共用助憶符之一,可以用來代表資料移動在各種單位之間(暫存器、記憶體位址)的指令。
2. ECALL 是 RISC-V Evironment Call 的意思。這個指令會觸發例外(exception),分成使用者模式環境呼叫例外(Environment-Call-from-U-mode)、作業系統模式環境呼叫例外(Environment-Call-from-S-mode)、機器模式環境呼叫例外(Environment-Call-from-M-mode)。這裡將會觸發的是作業系統模式環境呼叫例外(Environment-Call-from-S-mode),因為我們預期這個可執行檔是與 Linux 這類的作業系統映像檔同樣位階的系統。
3. 轉換權限等級(privileged mode)。以結果來說,這裡的 ECALL 將會造成權限等級轉換回到 M-mode 的 OpenSBI,由 OpenSBI 來服務這個呼叫。
4. 這個呼叫對應到的部分是,OpenSBI 中 lib/sbi/sbi_ecall_legacy.c 檔案內的 sbi_ecall_legacy_handler 函式裡的 SBI_EXT_0_1_CONSOLE_PUTCHAR 條件。OpenSBI 所支援的環境呼叫規格書在此處可參照。

  1. a6 暫存器代表該呼叫所屬於的集合。這裡令為 0 值,代表的是傳統(legacy)集合,實際上已經不建議使用。現在的 S-mode 系統軟體實踐都會避免使用傳統集合的環境呼叫,我們這裡只是暫且挪用其方便性。
  2. a7 暫存器代表該呼叫在該集合中的序號。這裡令為 1,代表在 控制台(console)印出一個字元。
  3. 該字元來自 a0 暫存器內的 ASCII 碼。這裡的 0x48,取的是代表 Hoddarla 的 H 字元。

啟動流程

給對於 OpenSBI 有一定了解,較進階的讀者:以下描述的是 Jump mode 的預設行為。本節想要描述的技術重點是如何跳躍到 S-mode 進入點,但如果單純修改 OpenSBI 跳躍位址,或者直接使用 dynamic mode 控制,都是可以考慮的方案。

回顧 Linux 的啟動流程(假設所有軟體都已經被載入在對的地方)作為對照的話,會是這個樣子:

  1. QEMU 的微型啟動程式,將流程帶到實體記憶體起始位址 0x80000000
  2. OpenSBI 自 0x80000000 開始動作,執行到尾聲時,跳轉到 0x80200000 並切換為作業系統模式
  3. Linux Image 檔在 0x80200000,繼續執行下去。(若只考慮無壓縮格式,Linux 的輸出物有兩種,一種是 ELF 檔,通常檔名為 vmlinux,一種是將 ELF 檔頭去掉且增加未初始化變數空間的 Image 檔,通常是透過 objcopy 工具獲得的,詳情可參照 Linux 的 Makefile。)
  4. 過不多久,Linux 就會打開虛擬記憶體的控制開關,而開始使用虛擬記憶體位址,也就是 pc 會從 0x802000XX 轉變為0xffffffe0000000XX

但回到 ethanol 映像檔,我們還有進入點的問題沒有處理。Golang 連結器給我們的產物裡面,程式碼區段的開頭是其他組件所在的位址,而非像 Linux 一樣,去除 ELF 檔頭之後也能夠直接擺在記憶體裡面執行。也就是說,我們仍然需要一個機制來幫助我們從 OpenSBI 的出口 0x80200000 過渡到對應到 0xffffff8000053418 這個虛擬記憶體位址的實體記憶體位址。

幾經考慮之後(詳情可以參照 ethanol/goto 資料夾下的檔案),筆者決定導入一個小程式序列在 0x80200000,它負責跳到 ethanol 映像檔真正的進入點。所以整個流程是

     PA                              VA
     +-------------+ 0x80200000
     | goto        |
     +-------------+ 0x80201000      +------------+ 0xffffff80_00001000
     | hoddarla    |                 | ELF        |
     | kernel ELF  |                 | Header     |
     +     ---     + 0x80202000      +------------+ 0xffffff80_00002000
     | hoddarla    |                 | ELF        |
     | kernel code |                 | .text      |
     ...
     | Entry       | 0x80255418      | Entry      | 0xffffff80_00055418
     ...

由圖可見,真正的進入點位址(-T 參數)重新調整為 0xffffff8000002000,這是因為這麼一來,在低位的 20 個位元就可以在實體記憶體與虛擬記憶體位址之間保持一致。

goto 的實作細節這裡就不贅述,只是很簡單的位址計算而已。當編譯完成 ethanol 映像檔之後,使用 readelf 工具取得進入點的虛擬記憶體位址,然後拆解這個位址以合成 goto.s 檔案,再將之製成一個跳躍用微小程式序列,好讓它能夠跳躍到真正的進入點去。

首次試跑!

由於我們在進入點處插入了能夠印出 H 字元的程式碼,因此我們預期的是,QEMU 模擬器啟動一個模擬的 RISC-V 系統,CPU 開始執行 OpenSBI 的部分。等到 OpenSBI 啟動完成,跳躍並轉換權限等級到映像檔進入點。然後印出 H 字元之後,Golang 繼續進行原本的流程,結果可能踩到某些預期的錯誤,系統因此進入錯誤的狀態,卡住或是亂跑之類的。

可以存取 github 以進行以下實驗。

$ make clean && make
GOOS=opensbi GOARCH=riscv64 go build -ldflags='-R 0x1000 -T -0x7fffffe000' ethanol/ethanol.go
make -C goto
make[2]: 進入目錄「/home/noner/FOSS/hoddarla/ithome/ethanol/goto」
./patch.sh
riscv64-buildroot-linux-musl-as goto.s -o goto.o 
riscv64-buildroot-linux-musl-ld -T ld.script goto.o -o goto
riscv64-buildroot-linux-musl-objcopy -O binary goto goto.bin
make[2]: 離開目錄「/home/noner/FOSS/hoddarla/ithome/ethanol/goto」
make[1]: 離開目錄「/home/noner/FOSS/hoddarla/ithome/ethanol」
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/hw,addr=0x80201000,force-raw=on 

OpenSBI v0.9
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name             : riscv-virtio,qemu
Platform Features         : timer,mfdeleg
Platform HART Count       : 4
Firmware Base             : 0x80000000
Firmware Size             : 124 KB
Runtime SBI Version       : 0.3

Domain0 Name              : root
Domain0 Boot HART         : 0
Domain0 HARTs             : 0*,1*,2*,3*
Domain0 Region00          : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01          : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address      : 0x0000000080200000
Domain0 Next Arg1         : 0x0000000082200000
Domain0 Next Mode         : S-mode
Domain0 SysReset          : yes

Boot HART ID              : 0
Boot HART Domain          : root
Boot HART ISA             : rv64imafdcsu
Boot HART Features        : scounteren,mcounteren,time
Boot HART PMP Count       : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count      : 0
Boot HART MHPM Count      : 0
Boot HART MIDELEG         : 0x0000000000000222
Boot HART MEDELEG         : 0x000000000000a109
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHHHHHHHHQEMU: Terminated

什麼?結果 H 字元就這樣一直印出?

小結

予焦啦!本章的實際產物可分為三個部分:

  1. Golang 內容改動:這也是實際存在 patch 資料夾下的內容。我們在 src/runtime/rt0_opensbi_riscv64.s 當中新增了 4 行組合語言,代表一個在控制台印出單一字元的 SBI 呼叫。
  2. Hoddarla 專案內容:我們使用 goto 這個中介迷你程式,並介紹 ethanol 進入點如何被串聯進啟動流程中。過程中我們也回顧了一些 ELF 工具的使用與常識。關於編譯後的產物的建置方面,也有 Golang 的連結器參數的介紹。
  3. Hoddarla 專案附帶的工具內容:大致上就是如何使用 QEMU 工具開機。

然而,懸而未決的問題是,最後觀察到的洗頻現象。我們的如意算盤明明是安插一個字元輸出,而後繼續往後走向 Golang 的執行期初始化流程才對。欲知如何,請待下回分解。


上一篇
予焦啦!產出可執行檔
下一篇
予焦啦!支援 RISC-V 權限指令與暫存器
系列文
予焦啦!Hoddarla 專案起步:使用 Golang 撰寫 RISC-V 作業系統的初步探索33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
EN
iT邦好手 1 級 ‧ 2021-09-13 22:58:23

文章好精彩,期待下一篇文章 XD

高魁良 iT邦新手 1 級 ‧ 2021-09-13 23:19:20 檢舉

感謝支持!我也很期待你的系列開跑!

0
fdgkhdkgh
iT邦新手 5 級 ‧ 2022-12-29 09:21:52

讚讚推推

我要留言

立即登入留言