iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 10
1

前情提要


昨日都在寫程式,馬不停蹄地寫,導致篇幅看起來沒有很多,但筆者卻實在是已經累炸啦!而且更累的是寫完之後滿滿都是 bug!正向思考術告訴我們這都是學習的機會,於是筆者也欣然接受默默摸著鼻子 debug。除錯到最後,架構也有部份更動,不得不花一些篇幅來解釋。

產出極簡 ELF 檔:完結篇


認錯

寫到後來、debug 到後來,實在不知道到底怎麼繼續前進。總覺得能做的都已經做了,也好好地清除一些傳統的陋習.strtab.shstrtab 區段開頭的 0 字元、開頭的空白區段、還有 .symtab 區段的第一個標籤是空白等等,那為什麼等到 bug 都清完之後,卻仍然沒有辦法讓 add.o 和主函式連結起來?

因為太狂妄,所以筆者必須承受這些磨練。查找規格之後發現,筆者之前自以為應該被掃進歷史垃圾堆的那些行為,其實都是規格中規定的內容。規格之所以難讀,都是因為它只描述 what,卻未必能夠讓人很快發現 why。結果就是讓筆者這樣猜疑心重又不夠聰明的後進無法心領神會。之前筆者一直以為那些開頭的空白是 GNU 的某些怪黑客想出來的 hack 技,結果竟然是標準,那麼也難怪會出現問題了。

此刻筆者已經好不容易修復了這支 as.go 程式,對於昨日以來的錯誤經驗,以下就簡略呈現:

  • readelf -S 的時候無法顯示正確的區段名稱:這是因為 .shstrtab 區段的處理錯誤
  • readelf -a 觀察 .symtab 內容的時候無法顯示正確的區段名稱:這是因為 .strtab 區段的處理錯誤
  • 無法連結,系統 ABI 不合:新增 ELF 檔頭中處理器相依的 Flags 設定,參考 RISC-V 的手冊之後依照工具鏈的設定處理就可以了。

簡單回顧與心得

有了 go 語言這樣強大的工具,資料結構的操作完勝舊時代的 C 語言,筆者卻仍然在 ELF 檔案格式(更精確地說,只有物件檔一種)之中載浮載沉,原因無他,就是對於規格實在是太無知了,寫了再多程式碼,兜出來的東西也會無法使用的。這裡筆者整理一些心得回顧一下,在設計 as 的時候最需要注意的部份。

  • .shstrtab 檔頭:每一個區段都應該把自己的名稱以 C 語言字串寫入到 .shstrtab 代表的區段。一開始就決定了這個部份使用可變長度的字串陣列來做,等到輸出時再一口氣攤平這些字串而成為連續字元。.strtab 區段也是如此。
  • 尊重規格:那些乍看之下空白無用的東西都該加。
  • 內部資料結構的設計,最後弄得疊床架屋,對於 as 來說可用是可用,但是也沒有什麼重複利用的可能性了。目前採取的結構如下:
type sec64 struct {
        header  elf.Section64
        content []string
}

type elf64 struct {
        header   elf.Header64
        sections map[string]*sec64
}

type asUtil struct {
        src     *os.File
        objFile *os.File
        obj     *elf64
        symtab  []*elf.Sym64
        shOrder []string
}

為什麼要這樣做?src 是輸入檔案、objFile 是輸出檔案,這應該沒有什麼問題。obj 是為了維護組譯過程中逐漸成型的 ELF 物件檔的整個結構;symtab 成員獨立存在的原因,是因為雖然 sec64 型別(用來描述 64 位元的區段的型別)有一個可變字串陣列的 content 成員可以紀錄區段內容(尤其是那些以字串為單位的區段),它卻不適合用來存放結構化內容如 .symtab 那樣的標籤結構,因此這裡先用醜陋的架構帶過去,直接宣告一個成員給 .symtab 使用。最後一個也很絕,留到下一點詳述。

  • 資料結構存取方法是該用列舉?序數?這個問題可以說是困擾筆者最大的部份,而且在輸出 ELF 檔的過程中一定會遭遇到這樣的思考。話說從頭,筆者在昨天以前的理解之中,ELF 格式沒有規定區段檔頭的位置,只要它們全部位在連續的記憶體區塊中即可;隨著這個寬鬆的規矩而來的是,我們甚至可以隨意擺放任何一個區段,只要區段檔頭有明確紀錄每一個區段位在何處即可。然而,筆者沒有理解到這些規矩的弦外之音,那就是我們需要一個序列可以用類似陣列的方式存取這些檔頭。這下子問題就大了,筆者在之前的架構設計中,一直都把區段理解成是一種用字串當作索引的資料,比方說使用 ".strtab" 字串可以讓我存取到該區段;但是,.symtab 區段檔頭的 Link 成員必須存放 .strtab 區段的編號,卻又哪裡來什麼編號可以存呢?更雪上加霜的是,go 語言的列舉功能(for ... range)雖然非常方便,卻為了可靠性考量刻意打亂順序,使得每一次的列舉順序都不會一致。於是才會放置 shOrder 可變字串陣列來紀錄每個字串的出現順序。

最後的結果是讓 main.c

#include<stdio.h>

extern int add(int a, int b);
int main(){
        printf("%d\n", add(1, 2));
}

add.s

.section .text
add:
        add a0, a0, a1
        ret
.global add
.end

完好地連結在一起:

IRONMAN $ mv as/go-binutils /tmp/as
IRONMAN $ /tmp/as -o ~/add.o ~/add.s
IRONMAN $ riscv64-unknown-linux-gnu-gcc ~/add.o ~/main.c
IRONMAN $ ls
a.out

並且經過筆者確認可以使用,真是可喜可賀,完成了一個部份啦!

筆者也會誠實地更新 github 上面的 go-binutils 專案,但雖然目前這一份可以動,品質卻仍相當差,留待後日改進。

額外加開:qemu 環境


筆者之前的環境架設,主要還是走一整套的 Linux 架設流程,搭配 spike 指令集模擬器。最近從 riscv 的 github repo 上面看來,qemu 的 RISC-V版本已經相當健全;更重要的是,使用 qemu 工具的話,還可以直接執行 userspace 程式來測試!這樣我們就可以免去重複的打包、開啟關閉模擬器的流程了。筆者簡單介紹架設步驟:

$ git clone https://github.com/riscv/riscv-qemu.git
$ cd riscv-qemu
$ mkdir build && cd build
$ ../configure --target-lists=riscv64-linux-user --disable-sdl --disable-vnc --disable-gtk --python=/usr/bin/python2
$ make

後面那一項是 python 工具第 2 版的路徑指定,請依照各位讀者的環境相應調整。編譯成功之後,就可以這樣使用

$ ./riscv64-linux-user/qemu-riscv64 ./a.out

如果出現 /lib/ld-linux-riscv64-lp64d.so.1: No such file or directory 字樣的話,表示之前 gcc 的編譯結果產出是動態連結執行檔,所以在 qemu 試圖執行的時候,會遇到這個情況。在 gcc 編譯時可以加上 -static 來避免這個問題。

最後結果分析:add.o


好不容易 debug 出來的產物,當然要看的更仔細一點了!筆者這裡使用 hexdump 工具,展示產出的 add.o

第一部份:ELF 檔頭

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 f3 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000030  05 00 00 00 40 00 00 00  00 00 40 00 05 00 01 00  |....@.....@.....|

比較重要的部份是我們在 0x28 處的 0x40 這個位址,代表區段檔頭的起始位址,總共有 0x05 個區段檔頭(0x3C),每一個的大小也是 0x40 bytes(0x3A),各個區段名稱存放的區段(.shstrtab)的區段檔頭索引是 0x01 (這就是 0x3E 存的內容)。整個 ELF 檔頭的大小是 0x40(存在 0x34 的地方),也就是說區段檔頭就要緊接在後,這與一般的實作是相反的,因為一般實作都會把區段先放置在檔案之中,最後才會是連續的區段檔頭。

第二部份:空區段檔頭

這一段就略過不看。

第三部份:區段名稱(.shstrtab)區段

00000080  01 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
00000090  00 00 00 00 00 00 00 00  80 01 00 00 00 00 00 00  |................|
000000a0  21 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |!...............|
000000b0  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

這個區段指向的位址是 0x180(0x98 的資料),總長度是 0x21 (0xA0 的資料內容),

00000180  00 2e 73 68 73 74 72 74  61 62 00 2e 73 74 72 74  |..shstrtab..strt|
00000190  61 62 00 2e 73 79 6d 74  61 62 00 2e 74 65 78 74  |ab..symtab..text|
000001a0  00                                                |.

各位讀者可以非常清晰的看見,這裡就是所有包含在這個檔案裡的區段名稱。GNU 的 as 所產生的物件檔中,區段的排列方式和名稱的順序沒有對應關係,但筆者這個版本是有的,因此第一個空字元就可以視為是空檔頭無須印出之後的隔離用空字元。0x80 位址的 1 代表著這個區段的名稱字串的開頭在名稱區段的第一個位元組,所以我們不難預期下一個區段檔頭會以 0x0B 開頭,因為那樣才會指向 .strtab 字串。

第四部份:字串表(.strtab)區段

000000c0  0b 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
000000d0  00 00 00 00 00 00 00 00  a1 01 00 00 00 00 00 00  |................|
000000e0  05 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

內容則是

000001a1     00 61 64 64 00                                 | .add.           

第五部份:標籤表(.symtab)區段

00000100  13 00 00 00 02 00 00 00  00 00 00 00 00 00 00 00  |................|
00000110  00 00 00 00 00 00 00 00  a6 01 00 00 00 00 00 00  |................|
00000120  48 00 00 00 00 00 00 00  02 00 00 00 02 00 00 00  |H...............|
00000130  08 00 00 00 00 00 00 00  18 00 00 00 00 00 00 00  |................|

這比較有趣,因為在 0x138 的地方指定了 0x18 這個值,代表這個區段的內容是結構化的,每一筆結構的長度是 24 bytes(0x18);總長度則是 0x48 (72 bytes),定義在 0x120 的地方。也就是說,我們會有三筆標籤的紀錄(筆者在 hex 中使用 '|' 符號將之分隔開來):

000001a6                    00 00  00 00 00 00 00 00 00 00  |      ..........|
000001b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00|00 00  |................|
000001c0  00 00 03 00 04 00 00 00  00 00 00 00 00 00 00 00  |................|
000001d0  00 00 00 00 00 00|01 00  00 00 12 00 04 00 00 00  |................|
000001e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00        |..............   

其中,第一筆完全空白,第二筆則是代表 .text 區段,第三筆不用說就是 add 函數了。0x1c2 的 0x03 代表 .text 這個標籤是一個區段,後面的 0x04 則代表這是指向第四個區段;add 的第一個位元組 0x01 代表它在 .strtab 中的定位,果然是 add 字串。

第六部份:程式碼(.text)區段

00000140  1b 00 00 00 01 00 00 00  06 00 00 00 00 00 00 00  |................|
00000150  00 00 00 00 00 00 00 00  ee 01 00 00 00 00 00 00  |................|
00000160  08 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000170  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

區段的內容是

000001ee                                             33 05  |              3.|
000001f0  b5 00 67 80 00 00                                 |..g...|

正是我們在 rvgc 函式庫中的回傳機器編碼。

小結


今日我們我們終於首次產出了能夠被連結的 ELF 物件檔,500 位元組上下,短小精悍!明日就讓我們更深入了解 RISC-V 指令集與 rvgc 函式庫的實作的其他實作吧。各位讀者我們明日再會!


上一篇
第九日:as 輸出物件檔之基礎架構實作
下一篇
第十一日:RISC-V 指令集架構介紹
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言