iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
0

前情提要


昨日打下一個簡單的基礎,我們大致完成了解析輸入檔案的進度;事實上,我們將處理結果存放在近似一般 ELF 檔案的行為,也可以視為在幫檔案輸出的部份鋪路。按照順序的話,接下來應該直接進入實作 rvgc 函式庫的部份,但是為了減少 debug 時間,筆者將本日的重心放在如何從昨日的基礎中,輸出一個可以用的 add.o 檔案。至於實際組合語言到機器碼的過程,就先把 8 bytes 的已知結果先當作輸出,明日再開始補齊 rvgc 函式庫的實作部份而取代。

輸入組合語言-續


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 除外。所以我們可以寫虛擬碼了:

  1. 依照 -o 命令列參數開檔。
  2. 設定 Shoff0x40(ELF 檔頭的長度),然後將整個 header 成員按照位元組次序輸出到檔案中。
  3. 宣告一個 fileOffset 變數,用來紀錄當前寫入檔案的偏移量。初始值是 ELF 檔頭大小加上所有區段檔頭的大小,也就是第一個區段應該置放的地方。
  4. 列舉 sections 檔頭
    1. 將當前的 fileOffset 作為這個檔頭的 Off 變數。
    2. 計算區段長度後,更新檔頭內的 Size 成員;寫入這個檔頭到檔案中。
    3. 使用 fseek 方法調整檔案內游標,寫入該區段。
    4. 更新 fileOffset,加上該區段的長度。
    5. 移動回下一個區段檔頭之檔案游標。

小結


今日我們大致完成了輸出的樣貌,但是由於筆者對於 go 語言的不熟悉,許多細微部份仍在 debug 中,主要是型別轉換的部份有太多需要注意的地方,導致今日還無法釋出可用的程式碼,但筆者相信距離一個真正的 ELF 檔輸出已經不遠了!明日,釋出可用輸出部份的同時,就讓我們深入了解 RISC-V 指令集與 rvgc 函式庫的實作吧。各位讀者我們明日再會!


上一篇
第八日:組語檔案架構處理
下一篇
第十日:as 初傳捷報
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言