昨日都在寫程式,馬不停蹄地寫,導致篇幅看起來沒有很多,但筆者卻實在是已經累炸啦!而且更累的是寫完之後滿滿都是 bug!正向思考術告訴我們這都是學習的機會,於是筆者也欣然接受默默摸著鼻子 debug。除錯到最後,架構也有部份更動,不得不花一些篇幅來解釋。
寫到後來、debug 到後來,實在不知道到底怎麼繼續前進。總覺得能做的都已經做了,也好好地清除一些傳統的陋習:.strtab
和 .shstrtab
區段開頭的 0 字元、開頭的空白區段、還有 .symtab
區段的第一個標籤是空白等等,那為什麼等到 bug 都清完之後,卻仍然沒有辦法讓 add.o
和主函式連結起來?
因為太狂妄,所以筆者必須承受這些磨練。查找規格之後發現,筆者之前自以為應該被掃進歷史垃圾堆的那些行為,其實都是規格中規定的內容。規格之所以難讀,都是因為它只描述 what,卻未必能夠讓人很快發現 why。結果就是讓筆者這樣猜疑心重又不夠聰明的後進無法心領神會。之前筆者一直以為那些開頭的空白是 GNU 的某些怪黑客想出來的 hack 技,結果竟然是標準,那麼也難怪會出現問題了。
此刻筆者已經好不容易修復了這支 as.go
程式,對於昨日以來的錯誤經驗,以下就簡略呈現:
readelf -S
的時候無法顯示正確的區段名稱:這是因為 .shstrtab
區段的處理錯誤
readelf -a
觀察 .symtab
內容的時候無法顯示正確的區段名稱:這是因為 .strtab
區段的處理錯誤
Flags
設定,參考 RISC-V 的手冊之後依照工具鏈的設定處理就可以了。有了 go 語言這樣強大的工具,資料結構的操作完勝舊時代的 C 語言,筆者卻仍然在 ELF 檔案格式(更精確地說,只有物件檔一種)之中載浮載沉,原因無他,就是對於規格實在是太無知了,寫了再多程式碼,兜出來的東西也會無法使用的。這裡筆者整理一些心得回顧一下,在設計 as 的時候最需要注意的部份。
.shstrtab
檔頭:每一個區段都應該把自己的名稱以 C 語言字串寫入到 .shstrtab
代表的區段。一開始就決定了這個部份使用可變長度的字串陣列來做,等到輸出時再一口氣攤平這些字串而成為連續字元。.strtab
區段也是如此。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
使用。最後一個也很絕,留到下一點詳述。
".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 專案,但雖然目前這一份可以動,品質卻仍相當差,留待後日改進。
筆者之前的環境架設,主要還是走一整套的 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
來避免這個問題。
好不容易 debug 出來的產物,當然要看的更仔細一點了!筆者這裡使用 hexdump 工具,展示產出的 add.o
:
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
函式庫的實作的其他實作吧。各位讀者我們明日再會!