完成了 rvgc 函式庫的 InstToBin
函式的轉譯功能之後,我們理論上應該可以支援更豐富的組合語言輸入檔了!今天就來驗證一下其他的組語檔案作為測試,然後也規劃一下系列文今後的發展方向。
承昨日,即使 add 指令透過 R-type 格式轉譯成功,ret 這個虛擬指令則終究無法在現在的狀況下組譯成為正確的機器碼。為此,我們先嘗試一下手動翻譯。也就是回答這個問題:ret 指令是從什麼變成的?從規格書上說,這實際上是 jalr 指令且不存放回傳位址的效應,
jalr zero, 0(ra)
在 rd = zero
的情況下,該行的下一個位址不會有任何有效的寫入,因為 zero 被鎖死在全零的狀態;而下一道指令時,pc 會跳轉到以 ra 暫存器為基準且 0 偏移的位址,也就是 ra 先前定義的部份。
如此修改之後,我們也多發現了原本程式中的兩個 bug(真多OTZ),修正如下。第一個是在我們早先利用正規表示法函式庫 regexp 解析指令的 preProxessLine
函式之中,
- rePunc := regexp.MustCompile(`,()`)
+ rePunc := regexp.MustCompile(`[,()]`)
筆者希望的效應是能夠把逗號和括弧都去掉,因此應該引用方括號的邏輯才能夠讓正規表示式成功配對。第二個則是在 rvgc 函式庫中處理 I-type 指令的部份,當時設立 isop
旗標就是為了分別嵌入整數和 rs1 暫存器的索引應該誰先誰後,結果原本的索引給反了。總之修正之後,就能夠成功並成功執行內容了。
來設計一個炫一點的組合語言程式吧!內容涵蓋我們之前介紹的所有指令種類。(結果又發現兩個 S-type 的 bug,出現在 rs1 和 rs2 暫存器位址混淆的情況,以及 sb
指令的 funct3
有問題)基本骨架先讓它長成這樣:
1 .section .text
2 _start:
3 addi sp, sp, -16 # 將堆疊指標暫存器 sp 向下挪移 16 bytes
4 addi t0, zero, 72 # 將 t0 的內容設為 0,
5 sb t0, 0(sp) # 然後把這個值放進 sp[0]
6 addi t0, zero, 105
7 sb t0, 1(sp) # sp[1] = 105
8 addi t0, zero, 33
9 sb t0, 2(sp) # sp[2] = 33
10 addi t0, zero, 10
11 sb t0, 3(sp) # sp[3] = 10, 至此為止是在設定 sp 中的四個字元
12 addi a0, zero, 1 # 第一個參數,0 代表的是標準輸出終端機
13 add a1, zero, sp # 第二個參數,代表要輸出的 buffer 起始位址
14 addi a2, zero, 4 # 第三個參數,代表輸出 4 個字元
15 addi a7, zero, 64 # 設定 write 系統呼叫號碼
16 ecall # 正式執行系統呼叫
17 addi a0, zero, 777
18 addi a7, zero, 93 # 設定 exit 系統呼叫號碼,以 777 為回傳值
19 ecall # 程式中止
20 .end
一寫就知道虛擬碼的重要性了...單以 li(讀取整數到暫存器)、mv(將一個暫存器的資料搬至另外一個)這兩個指令看起來都是加法來講,就一定得有虛擬碼的設計才行。不過筆者這系列還是以學習 ELF 格式為第一優先順位,接下來可能還要請各位讀者委屈一下了。
這份程式碼可以這樣執行:
$ cp go-binutils /tmp/as && /tmp/as -o static.o static.s && riscv64-unknown-linux-gnu-ld -o a.out static.o && /riscv-tools/riscv-gnu-toolchain/riscv-qemu/build/riscv64-linux-user/qemu-riscv64 ./a.out
Hi!
為什麼叫做 static.s?因為我們沒有與基本的 C runtime(執行期環境)還有標準 C 函式庫連結,而且直接使用預設進入點標籤的 _start
作為目前唯一的函式。最決定性的,則是我們採用了靜態的連結方式,無須在執行期連結動態函式庫就已經可以令它成功運作。
再沒有任何重定向的功能實作之前,我們缺乏在組語檔案中操作標籤的能力,但即使如此,我們還是可以設計簡單的迴圈。為求閱讀方便,迴圈內容多一個縮排:
...
11 sb t0, 3(sp)
12 addi t1, zero, 4 # 將 t1 暫存器設為迴圈變數 4
13 addi a0, zero, 1
14 add a1, zero, sp
15 addi a2, zero, 4
16 addi a7, zero, 64
17 ecall # 剛才的系統呼叫內容,沒有變化
18 addi t1, t1, -1 # t1--
19 bne t1, zero, ff4 # if t1 != zero jump ... ff4???
...
這個 ff4
是什麼意思呢?字面上來說,在這裡的 3 個 16 進位數字是筆者想要表達我想要嵌入 B-type 指令中的 12 bit 整數。這裡取 ff4 乍看之下是 -12 的意思,但別忘記了 B-type 指令的判讀方式是將整理好的 12 個 bit 當作 [13:1],然後結尾再補上一個零:因為標準裡面確保 RISC-V 平台的指令永遠都會在整數位置上,所以最後一個 0 bit 不需要花空間來標記。也就是說,這裡的 -12,實際上是 -24 的意思,考量到 RV64I 的標準整數指令集都是 4 bytes,-24 也就是回推 6 個指令的意思了,剛好是 13 行的設置參數起算。
go 語言的 strconv.ParseInt 函式似乎不會幫我們自動解讀 16 進位的正負值,因此這裡只好把原本設定的 12 bit 再多一個 bit;或是,雖然我們將這 12-bit 認知為有號整數,但反正要進行的是 bit 位置操作,因此當作有號整數也無所謂,改用 strconv.ParseUint 也能夠解決這裡的問題。
20 lui t2, 44434 # 將 t2 暫存器的前 20 個 bit 設成 0x44434 的模式
21 addi t2, t2, 577 # 將後 12 bit 設成 557,也就是 0x241 的模式
22 sw t2, 0(sp) # 引用存入 4 bytes 的指令
23 addi a0, zero, 1
24 add a1, zero, sp
25 addi a2, zero, 4
26 addi a7, zero, 64
27 ecall # write 系統呼叫輸出:ABCD
因為 0x41 正好是 A 字元的 ASCII 碼,因此這裡的操作成立。
今日為止,我們將約莫上週的工作規劃(as 與 rvgc 函式庫)完成了;當初之所以這樣規劃是因為筆者心裡認識到,處理 ELF 格式的這些工具程式,並非每一個都像 readelf 一般浮光掠影、只須按照檔頭規格爬一爬就還能夠交差。對於檔案裡面的內容,也是該下一番功夫好好了解的吧!豈有讀通 TCP/IP 封包就能理解 websocket 的道理呢?所以,筆者當時心裡想的是 objdump 的支援,但是評估一陣之後認定解讀機器碼的函式庫(後來被我們命名為 rvgc)是系列文下階段的核心,又因為組譯器比較炫,所以這麼選擇進行至今;結果中間遇到亂流:bug 橫生、規格沒看清楚,弄到能夠喘息的這時候,別人都在過歡樂跨年假,筆者卻不能不對系列文負責,就這麼過去了半個系列。
所以接下來呢?
如果可以全部完成的話當然是最理想狀態,但是按照之前的經驗,也許只能做到 objdump 也不一定。
.a
檔案,但是由於 go 語言自己處理物件檔的格式不是 ELF,因此這個部份就略過。所以筆者用 go 作 ELF 工具可說是拼裝車一般,畢竟 go 的慣例只有在最後產出的東西是 ELF。
總而言之,原本預期的規劃想得太過天真,不得已只好在旅途中不斷修正方向,還請各位讀者見諒。明天起我們就開始實作 size 工具程式吧!