昨日打下一個簡單的基礎,我們大致完成了解析輸入檔案的進度;事實上,我們將處理結果存放在近似一般 ELF 檔案的行為,也可以視為在幫檔案輸出的部份鋪路。按照順序的話,接下來應該直接進入實作 rvgc 函式庫的部份,但是為了減少 debug 時間,筆者將本日的重心放在如何從昨日的基礎中,輸出一個可以用的 add.o 檔案。至於實際組合語言到機器碼的過程,就先把 8 bytes 的已知結果先當作輸出,明日再開始補齊 rvgc 函式庫的實作部份而取代。
按照既有的 Run 框架,我們會在取得 add 這個標籤之後,隨即看到兩行的組合語言。這個部份我們可以先觀察原本的 add.o 的內容(這是 GNU 的 objdump -d):
0000000000000000 <add>:
0: 00a58533 add a0,a1,a0
4: 00008067 ret
所以,我們可以先規劃一個 API,他的輸入是字串陣列型別的指令結構,輸出則是一個字元陣列,代表機器碼指令的實際樣貌。這個 API 我們暫且開作 Cmd2Hex:
func (reu *asUtil) inst(d []string) {
reu.raw[currentSection] = append(reu.raw[currentSection], rvgc.Cmd2Hex(d))
}
rvgc 函式庫裡的實作暫且規劃如此:
func Cmd2Hex(cmd []string) []byte {
if cmd[0] == "add" {
return []byte{'\x33', '\x85', '\xa5', '\x00'}
} else {
return []byte{'\x67', '\x80', '\x00', '\x00'}
}
}
為什麼這個順序是反過來的呢?因為我們現在是小頭順序,所以讀取順序會從比較低的位數開始。總之,這麼一來我們在解析的過程就只剩下標籤的處理了!
關於標籤我們必須有個認知,那就是 .symtab 區段就是為了儲存標籤相關的資訊。使用 GNU readelf,我們可以看到 add.o 的標籤表內容如下:
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 SECTION LOCAL DEFAULT 1
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 add
老樣子,又有一個全部都是 0 值的,但是筆者這裡也沒有打算加它。最後一個顯然是我們的 add 函數,編號 1~3 的標籤又代表什麼呢?佐以 objdump,我們可以看到另外一個面向:
$ riscv64-unknown-linux-gnu-objdump -x ./add.o
./add.o: file format elf64-littleriscv
./add.o
architecture: riscv:rv64, flags 0x00000010:
HAS_SYMS
start address 0x0000000000000000
...
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 g .text 0000000000000000 add
所以其實,除了 add 之外的 3 個 entry 就都是 section 名稱。至於表格的那些屬性,分別是就都是 section 名稱。
一番考察之後,可以發現 debug/elf 函式庫也有包含標籤對照表的結構體定義,
type Sym64 struct {
Name uint32 /* 標籤名稱在 `.strtab` 的位置 */
Info uint8 /* 型態(區段?函式?變數?)、連結度(全域?區域?) */
Other uint8 /* Reserved (not used). */
Shndx uint16 /* 該標籤所在的區段索引號碼 */
Value uint64 /* 該標籤代表的值(函數或變數的位址) */
Size uint64 /* 該標籤代表的物件的大小 */
}
每個標籤的 Name 成員變數之於 .strtab 的操作方式與定義顯然與區段之於 .shstrtab 的操作方式差不多,所以我們這裡也要有類似的初始化階段,以及一個類似的 addLabel 函式用以更新 .strtab(新增一個代表該標籤的字串,比方說 add),然後在 RUN 中遇到 .global 組譯器選項時必須更改標籤屬性。
筆者試過如果沒有
.global那一行的話,add函數將被當作局部函數,進而使得後續連結期間main函數找不到任何一個add函數與之連結。
考量到 .symtab 的特殊性,筆者決定在 asUtil 結構之中加入一個 symtab 變數,型別為字串(以標籤本身當作索引)到 Sym64 的對照表,,等到輸出時再採取合適的手段輸出。這個成員變數在執行時遇到冒號結尾時,就會將前面的字串判斷為標籤,然後呼叫 addLabel 函數:
func (reu *asUtil) addLabel(lab string) {
reu.raw[".strtab"] = append(reu.raw[".strtab"], lab)
reu.symtab[lab] = elf.Sym64{
Name: currentOffsetStr,
Info: elf.STINFO(elf.STB_LOCAL, elf.STT_FUNC),
Shndx: currentSection,
}
currentOffsetStr += len(lab) + 1
}
其中,可以看到與 .shstrtab 差不多的處理方式,因為大多數的標籤都是要放在這裡的。然後會以傳入的標籤當作索引,初始化一個新的 symtab 元素。值得注意的是,Info 變數由兩個真正的變數共用,前者是 Bind,代表一個標籤在連結時的性質;這裡初始化為區域性(LOCAL),代表其他物件無法利用這個標籤。後者則是 Type,代表這個變數的型態。這裡筆者在初始化 Info 成員變數時取巧,先假定所有東西都是函數型態,而這當然是不正確的,日後有時我們會處理到變數,到時候就應該設定這個屬性為物件才行。
我們在標籤處理的最後一塊拼圖,就是有時候會遇到 .global 的組譯器選項時,我們必須將連結性質更新為全域性(GLOBAL),相應的邏輯實作在 dire 函數的新增內容裡面:
...
case ".global":
if len(d) != 2 {
return false, errors.New("Syntax error: label not specified!")
}
reu.symtab[d[1]].Info = elf.ST_INFO(elf.STB_GLOBAL, elf.STT_FUNC)
...
到這裡為止,add.s 裡面的每一行都有被處理到了。可以準備輸出啦!
經過了這一堆區段參數的轟炸之後,我們可以清醒一下,規劃輸出的內容會有哪些東西。
首先必然是 ELF 檔頭。裡面有哪些東西沒有設定?現階段只剩下 Shoff,也就是區段標頭的起始偏移量還沒有決定了。我們這裡其實可以直接把區段檔頭接到 ELF 檔頭後面,然後將它們連續輸出。等到輸出完所有區段檔頭之後,再將過程中設置的區段一個一個寫到目標檔案之中。在 add.o 的情況,只有 .shstrtab、.strtab、.symtab、.text 等四個區段有真實的內容。但是,sections 成員是一個有序的區段檔頭陣列,raw 成員卻是一個對照表型別的變數,雖然 go 語言提供用 for 迴圈列舉對照表的方法,但他們兩個列舉起來的順序可能會不一樣,這卻該怎麼辦呢?沒有辦法,只好在列舉 sections 成員的時候,從 Name 成員取得 .shstrtab 裡面的字元索引之後,再用所屬的字元當作 raw 的索引存取內容;而且要記得,.symtab 除外。所以我們可以寫虛擬碼了:
-o 命令列參數開檔。Shoff 為 0x40(ELF 檔頭的長度),然後將整個 header 成員按照位元組次序輸出到檔案中。fileOffset 變數,用來紀錄當前寫入檔案的偏移量。初始值是 ELF 檔頭大小加上所有區段檔頭的大小,也就是第一個區段應該置放的地方。sections 檔頭
fileOffset 作為這個檔頭的 Off 變數。Size 成員;寫入這個檔頭到檔案中。fseek 方法調整檔案內游標,寫入該區段。fileOffset,加上該區段的長度。今日我們大致完成了輸出的樣貌,但是由於筆者對於 go 語言的不熟悉,許多細微部份仍在 debug 中,主要是型別轉換的部份有太多需要注意的地方,導致今日還無法釋出可用的程式碼,但筆者相信距離一個真正的 ELF 檔輸出已經不遠了!明日,釋出可用輸出部份的同時,就讓我們深入了解 RISC-V 指令集與 rvgc 函式庫的實作吧。各位讀者我們明日再會!