iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
1
Software Development

與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包系列 第 29

第二十九日:as 強化(下)

前情提要


昨日為 as 的功能加強起了個頭,今天讓我們來完成它吧!

回顧:實作目標


  • 理解 call 虛擬指令,將之轉換為 auipc+jalr 指令配對
  • 製造相對應的重定區段
    • 有一個 R_RISCV_NONE 的空重定
    • 一個 R_RISCV_CALL 的重定 Rela64 結構內容
      • Off 成員在 call 的位置
      • Info 成員是 R_RISCV_CALL 重定型態與該標籤編號的複合體
      • Addend 成員的值是 0

製作重定區段


這個部份我們可以參考之前為 as 加入標籤區段的經驗。回顧一下,當時的作法是:

func (asu *asUtil) addLabel(lab string) {
        asu.obj.sections[".strtab"].content = append(asu.obj.sections[".strtab"].content, lab)
        asu.symtab = append(asu.symtab, &elf.Sym64{
                Name:  currentOffsetStr,
                Info:  elf.ST_INFO(elf.STB_GLOBAL, elf.STT_FUNC),
                Shndx: asu.obj.header.Shnum - 1,
        })

        currentOffsetStr += uint32(len(lab) + 1)
}

我們透過一個額外的成員變數 symtab (這是一個可變長度陣列)來彰顯標籤區段的特殊性。每一次新增標籤,就將之置放在這個區段之中。事實上,以我們的目的:call write 指令轉變為重定區段內的一筆資料而言,我們也必須考慮經過 addLabel 的過程,因為對於我們的 fin2.o 檔來說,它會把 write 當作一個字串存在 .strtab 之中,而將它的相關屬性存在 .symtab 之中,

$ riscv64-unknown-linux-gnu-readelf -s ~/fin2.o

Symbol table '.symtab' contains 6 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    3
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    1 _start
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND write

我們的 write 函式在這裡被定義成 NOTYPE、連結的屬性是 GLOBAL、值和大小都是未知、所屬的區段是 UND 未定義!最後則是將 .rela.text 區段裡面的一個結構設置完成,這樣應該就能夠被 ld 使用來作連結了。

新增 .rela.text 區段


理論上,這裡應該要在整個檔案完結卻沒有完成所有標籤的定義的時候才新增這個區段,但是筆者現在已經認定 go-binutils 這個專案必須面臨大型重構,所以我們這裡就用快而骯髒的方式把它作掉吧。

首先我們先來解決檔頭的問題。.rela.text 檔頭所需要的資訊有這些項目:

$ riscv64-unknown-linux-gnu-readelf -S ~/fin2.o
There are 8 section headers, starting at offset 0x170:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
...
  [ 2] .rela.text        RELA             0000000000000000  00000108
       0000000000000030  0000000000000018   I       5     1     8
...
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

大部分資訊都能夠在可以在 addSection 函式中完成。但是大家若回想 .symtab 時的經驗,當時在輸出前才決定它自己如何將 Link 屬性連結到 .strtab 區段檔頭。重定區段的 Link 必須連結到相應的標籤區段(本例的 5 正是 .symtab),而 Info 必須連結到相應的程式區段(本例的 1 正是 .text)。所以以上資訊分別應該這樣解決:

func (asu *asUtil) addSection(sec string) error {
...
        switch sec {
        case "":
                thisSec.header.Type = uint32(elf.SHT_NULL)
...
        case "rela.text":
                thisSec.header.Type = uint32(elf.SHT_RELA)
                thisSec.header.Flags = uint64(elf.SHF_INFO)
                thisSec.header.Addralign = 0x8
                thisSec.header.Entsize = 0x18
        }
...

這是固定屬性的部份,非得到最後一刻決定不可的屬性則是:

                if name == ".strtab" {
                        asu.obj.sections[".symtab"].header.Link = uint32(i)
                        asu.obj.sections[".symtab"].header.Info = 2
                } else if name == ".symtab" {
                        asu.obj.sections[".rela.text"].header.Link = uint32(i)
                } else if name == ".text" {
                        asu.obj.sections[".rela.text"].header.Info = uint32(i)
                }

第一個 .strtab 的判斷之內是之前 .symtab 使用的部份。這裡我們為 .rela.text 新增了兩個判斷。如此一來,檔頭的資訊就不怕沒有了。

重定區段內容

如同其他區段,我們會需要在開頭的地方安插一個虛無的零區段(之前 .symtab 的零區段安插在第零個區段標頭的所在,所以這裡如法炮製):

        case "":
                thisSec.header.Type = uint32(elf.SHT_NULL)
                asu.symtab = append(asu.symtab, &elf.Sym64{
                        Name:  0,
                        Info:  elf.ST_INFO(elf.STB_LOCAL, elf.STT_NOTYPE),
                        Shndx: 0,
                })
+               asu.rela = append(asu.rela, &elf.Rela64{
+                       Off:   0,
+                       Info:  elf.R_INFO(0, elf.R_RISCV_NOTYPE),
+                       Shndx: 0,
+               })

再來就是最關鍵的部份了:我們該如何在看見 call write 的時候,讓 as 這隻程式能夠插入一個重定區段?

與 call write 正面對決

我們還是得從判斷開始一步一步走,最適合的切入點當然就是處理任何指令的 inst 函式。我們在這裡會取得 call write 的指令本體,所以在這裡,我們已經可以透過之前調整的 rvgc.InstToBin 函式判斷出一道指令是否含有任何重定;之後,將後面的 write 透過 addLabel 函式走正常的新增流程。然後我們可以創建一個 Rela64 結構,使之指向剛才為 write 建立的標籤結構。這樣應該就差不多了吧?

inst 函式之中加入:

func (asu *asUtil) inst(d []string) {
        b, r := rvgc.InstToBin(d)

        asu.obj.sections[currentSection].content = append(asu.obj.sections[currentSection].content, string(b))

        switch r {
        case elf.R_RISCV_NONE:
                break
        case elf.R_RISCV_CALL:
                asu.obj.sections[".strtab"].content = append(asu.obj.sections[".strtab"].content, d[1])
                asu.symtab = append(asu.symtab, &elf.Sym64{
                        Name:  currentOffsetStr,
                        Info:  elf.ST_INFO(elf.STB_GLOBAL, elf.STT_NOTYPE),
                        Shndx: 0,
                })
                asu.rela = append(asu.rela, &elf.Rela64{
                        Off:   2,
                        Info:  elf.R_INFO(uint32(len(asu.symtab)-1), r),
                        Addend: 0,
                })

                currentOffsetStr += uint32(len(lab) + 1)
        }
}

原本只有最前面的加入組譯結果的兩行,後面的 switch 結構則是為了判斷重定使用的東西。由於 addLabel 的流程假設了該標籤的性質,因此我們這裡將 addLabel 函式的內容複製出來手動修改。Info 成員的部份,由於剛加入 .symtab,所以可以直接使用 len 內建函數,**但是要知道的是這只是便宜行事,這裡應該要先搜尋過 .symtab 才可以下手!**總之我們先這麼做了;重定型態的部份當然是放入由 InstToBin 判斷得到的 r,也就是 RIRISCV_CALL。但是為什麼 Off 成員的值是 2 呢?因為筆者完全忘記這件事情啦!

當前 offset

為了要得到當前的 offset,我們必須從組譯開始,就開始累計:**4, 8, 12, 16, ...**這樣子計算下去。但是之前我們完全沒有考慮這點,所以這裡筆者選擇新增一個全域成員 currentOffset,讓它從零開始紀錄當前的 offset 所在。

func (asu *asUtil) inst(d []string) {
        b, r := rvgc.InstToBin(d)
+       currentOffset += len(b)

如此一來就可以拿來使用在 asu.rela 成員的新元素之中了。然而,以上工作之外,我們還需要最後的輸出部份。

輸出

這個部份我們也是可以參考 .symtab 的輸出方法。

        case ".symtab":
                for _, syment := range asu.symtab {
                        var binbuf bytes.Buffer
                        binary.Write(&binbuf, binary.LittleEndian, syment)
                        temp, _ := asu.objFile.Write(binbuf.Bytes())
                        size = size + uint64(temp)
                }
+       case ".rela.text":
+               for _, rent := range asu.rela {
+                       var binbuf bytes.Buffer
+                       binary.Write(&binbuf, binary.LittleEndian, rent)
+                       temp, _ := asu.objFile.Write(binbuf.Bytes())
+                       size = size + uint64(temp)
+               }

這樣應該就可以了吧!來試試看吧~

跑!


$ make run
/riscv-tools/riscv-gnu-toolchain/riscv-qemu/build/riscv64-linux-user/qemu-riscv64 ./fin
ABCD

小結


今日完成了可以連結的物件檔。剩下最後一天,很遺憾已經沒有時間可以實作連結器了,也很抱歉讓各位讀者期待了。明日,請讓筆者回顧一下過去這 30 天的奇幻旅程,以及未來的方向。感謝大家!


上一篇
第二十八日:as 強化(上)
下一篇
第三十日:結語
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言