iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 15
0

前情提要


完成了 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 作為目前唯一的函式。最決定性的,則是我們採用了靜態的連結方式,無須在執行期連結動態函式庫就已經可以令它成功運作。

測試:branch 指令

再沒有任何重定向的功能實作之前,我們缺乏在組語檔案中操作標籤的能力,但即使如此,我們還是可以設計簡單的迴圈。為求閱讀方便,迴圈內容多一個縮排:

...
 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 也能夠解決這裡的問題。

測試:U-type

 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 橫生、規格沒看清楚,弄到能夠喘息的這時候,別人都在過歡樂跨年假,筆者卻不能不對系列文負責,就這麼過去了半個系列。

所以接下來呢?

打算實作的 binutils 工具程式及它們的功能集合

  • 小工具
    • size:取得區段大小
    • strip:移除指定區段、移除所有標籤
    • nm:展示 ELF 檔中的 symbol
  • 中型工具
    • objcopy:取得任意區段內容、將一般檔案轉換成 ELF 檔的功能
    • objdump:反組譯程式區段的功能
  • 大型工具
    • ld:連結程式使之成為可執行檔

如果可以全部完成的話當然是最理想狀態,但是按照之前的經驗,也許只能做到 objdump 也不一定。

不打算實作的 binutils 工具程式及理由

  • addr2line:這個需要 debug 訊息的輔佐才有辦法在給定位址和執行檔之後找到原始碼的位置對照關係,所以這個部份的實作在這個系列應該是無法來得及完成了。
  • ar:這是操作靜態函式庫的工具程式。筆者原本很期待這個功能,因為 go 語言自己處理函式庫的方式也是一堆靜態的 .a 檔案,但是由於 go 語言自己處理物件檔的格式不是 ELF,因此這個部份就略過。

所以筆者用 go 作 ELF 工具可說是拼裝車一般,畢竟 go 的慣例只有在最後產出的東西是 ELF。

  • strings:和 ELF 格式沒有特殊關聯,沒有興趣實作。
  • c++filt:對 C++ 沒有興趣。
  • 其他諸般與 windows 相關的工具程式:筆者已經不用 windows 許多年了。

總而言之,原本預期的規劃想得太過天真,不得已只好在旅途中不斷修正方向,還請各位讀者見諒。明天起我們就開始實作 size 工具程式吧!


上一篇
第十四日:實作 rvgc 函式庫之二
下一篇
第十六日:使用 size 觀察各區段大小
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言