昨日打下一個簡單的基礎,我們大致完成了解析輸入檔案的進度;事實上,我們將處理結果存放在近似一般 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
函式庫的實作吧。各位讀者我們明日再會!