昨日解析了整個環境建置流程,也簡單介紹了其中元件扮演的角色。今天我們則要開始使用這個環境來實戰了!
riscv 社群針對 go 語言有一個移植的分支,可以透過以下方式下載(相關訊息來自 riscv/riscv-go 的 github 頁面):
$ git clone https://review.gerrithub.io/riscv/riscv-go riscv-go
$ cd riscv-go
$ git checkout riscvdev
$ export GOROOT_BOOTSTRAP=/usr/lib/go
$ cd src
$ ./make.bash
若是成功,則會看到
---
Installed Go for linux/amd64 in /riscv-go
Installed commands in /riscv-go/bin
的兩行。/riscv-go/bin/go
就是我們可以拿來作 cross-compile 開發的 go 語言檔案。所謂 cross compile 意指我們所使用的編譯平台與目標平台有所差異,常見於嵌入式系統無法自己為自己編譯工具鏈或其他程式的時候。比方說,我們昨日環境架設的第一步中,事實上就是在建立 riscv 的 cross compilation 工具鏈。
有了這個 go 檔,我們就可以拿來與世界打招呼了。先準備一個文字檔案,命名為 main.go
:
package main
import "fmt"
func main () {
fmt.Println("Hello, World!")
}
然後可以這樣測測看:
$ GOARCH=amd64 GOOS=linux /riscv-go/bin/go build main.go
$ ./main
這只是用來非常快速地讓讀者諸君感受一下 go 語言的一些基本特徵,並且很簡單的在 host(假設是 x86_64的架構)執行一次。
再來就是 cross 編譯這個程式並在模擬器上執行:
$ GOARCH=riscv GOOS=linux /riscv-go/bin/go build main.go
$ cp ./main /home/riscv/rootfs/ # 將編譯好的 riscv 程式放置到根目錄底下
$ cd /riscv-linux && make ARCH=riscv # 將 Linux 重編一次,這是為了把修改過的根目錄重新包裝回 initramfs
$ cd /riscv-tools/riscv-pk/build && make && make install # 將 bbl 重編一次,打包新的 Linux
$ /home/riscv/bin/spike /home/riscv/riscv64-unknown-elf/bin/bbl # 啟動環境
...
# ./main # 執行剛編好的程式!成功!
以上的絕對路徑都是在 docker 環境中操作的結果。若有自行建置環境的讀者,請自行處理路徑的連結。
再來,我們終於要把系列標題中的最後一塊拼圖納入進度了,也就是 ELF 檔案格式本身!這個階段我們介紹一下 Go 語言對於 ELF 檔案的支援。具體來說,我們將會使用的是 go 語言的 debug/elf 函式庫。
瀏覽了一下發現琳瑯滿目,不知道從何下手,怎麼辦呢?其實筆者也沒有比各位多了解太多太深,畢竟這是預計要在接下來的幾天要好好學習的目標嘛!要弄熟這個東西,最簡單的方法就是直接 call 看看準沒錯了。
在這個函式庫中,提供的 type 其實也不多,我們就先來研究一下 type OSABI
這個項目好了。從 OSABI
的連結點入可知,它歸屬於 ELF 格式的檔頭 FileHeader
,又,這個檔頭是 File 型別當中的一個子型別。所以我們可以寫一個簡單的程式如下:
package main
import (
"fmt"
"debug/elf"
)
func main() {
fp, _ := elf.Open("main")
fmt.Println(fp.FileHeader.OSABI.GoString())
defer fp.Close()
}
首先我們引用 elf
函式庫的 Open
方法,開啟一個名叫 main
的 ELF 檔;然後按照型別的階層使用 .
運算子存取,這應該是各大常用語言的使用者都不會陌生的一種語法。然後使用之前 Hello World 範例時也用過的 fmt.Println
方法將之轉為 GoString 型別的結果輸出。
筆者對於 go 語言的著墨大概就是這樣的深度而已。若有讀者對於 go 不熟悉,而想要一舉多得透過這個機會學習 go 語言的話,請留言或是私信通知筆者。感謝!
$ go build main.go
$ ./main
elf.ELFOSABI_NONE
這個是什麼意思呢?搭配文件的內容可以知道,這就是在 GNU/Linux 系統中也頗為常見的 System V 的 OSABI 型態。
有了 go 語言的這個函式庫,我們可以省去非常多的麻煩。一般而言,會對 ELF 檔感興趣的使用者多半是整日與 C 語言為伍的系統工程師,但是要是要筆者用 C 處理 ELF 檔,那還實在是太累了。因此這個系列使用 go 語言來開發 binutils 專案的功能,然後更新到 github 上面。將 go 語言環境佈建好之後,接下來介紹一下目前的 go-binutils 專案大致的架構,明天再開始 readelf 這塊 ELF 世界的敲門磚。
各位讀者可以在 github 載到這份原始碼。而且最棒的是,裡面還有附上 Makefile
檔案可以自動建置!然而如果真的直接按照這個序列執行:
$ git clone https://github.com/NonerKao/go-binutils.git
$ cd go-binutils
$ make
可能會發生一堆看起來很恐怖的 cannot find package 錯誤!這就是因為沒有尊重 go 語言的開發慣例的緣故。
我們在寫程式時,總是得之於人者太多,所以幾乎每一種能夠實用的程式語言都有引用他人函式庫的功能。Go 語言就是昨日也曾看過的 import
保留字,但由於我們使用的只有 fmt
和 debug/elf
兩個內建的函式庫,因此不會遇到上節可能遇到的錯誤。若是稍微瞄一下 main.go
的引用部份,我們會看到:
import (
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/NonerKao/go-binutils/common"
"github.com/NonerKao/go-binutils/readelf"
)
後面兩行,想當然爾是要依賴 github 上的函式庫 repo 囉?這麼想只能算是對一半。通常這的確表示 main.go
需要的 common
及 readelf
函式庫依賴那個 repo 位址,但是 github 專案有可能會不定時更新,一份程式碼依賴著可能動態改變的函式庫,難道內部邏輯都不會崩解嗎?所以,那其實只是個參考的索引,通常原意是這份程式有賴於線上的非官方函式庫的一些功能,但實際上透過內建的方法抓取整個 repo 時,go 語言其實會把那些非官方的部份都存放到本地端的 GOPATH
環境變數存放的路徑之下,然後真正要建置套件時,搜尋的就是那個地方。
所以,真正的流程應該比較近似這個方法:
$ export GOPATH=/some/path/
$ go get github.com/NonerKao/go-binutils # 放置在 $GOPATH/src/github.com/NonerKao/go-binutils
$ cd $GOPATH/src/github.com/NonerKao/go-binutils
$ make && make install # 建置並將產物放在 $GOPATH/bin/ 之下
GNU 的 binutils 專案和 Unix 工具程式(utility)哲學基本上類同,都是希望一個工具程式本身不要太大、太多功能,而又希望他們組合起來可以靈活組合運用。其中各個工具至今都已經累積了許多實戰經驗,在各種軟體開發的場合被廣大群眾所使用。這些工具程式除了標準的 C 函式庫,依賴的還有一個 bfd 函式庫負責處理組合語言、架構相依的底層實作,是 binutils 和 gdb 的重要組成之一。
有別於傳統的作法,筆者不打算採取這樣的架構設計。理由是,go 語言一般來說為了部署的速度,生成的二進位檔預設都是靜態連結的。如果採取上述的設計,靜態連結的結果,這些工具程式整體佔用的空間將會比 GNU 的空間還要多花很多倍。雖然現在硬碟空間沒有那麼昂貴,但是佔用空間這件事情想到仍然令人不甚愉快。
彷彿聽到各位在反問,「那麼,你又有什麼高見?共用的關鍵部份使用函式庫,各個工具程式分別釋出,就算這不是唯一作法,也絕對是標準作法吧!」是的,還有一種 busybox 模式,也就是一個單一的程式,隨著自己被呼叫的方式不同,而表現出涵蓋 sh, core-utils, 系統工具、網路工具等不同的程式的行為。有興趣的讀者可以檢驗一下我們之前建立好的開發環境的 /bin
資料夾,就會發現各式各樣的工具程式如今都成了軟連結,連到唯一的可執行檔,busybox。
之所以可以作到這點,是因為利用了第 0 個命令列參數,也就是指令名稱本身,讓程式行為隨著這個名稱而有所不同。
而這正是筆者打算採取的設計模式,目前仍然只有骨架而已,但是為了未來的擴充,筆者也是使出渾身解數,當作一個架構規劃的練習。
整體來說,go-binutils 專案的架構預計分為三個部份:
我們可以在 $GOPATH/bin
之下得到 make install
指令的產出,目前有以下三個可以執行的檔案(其中後兩者是軟連結):
以下各節,我們就以 readelf 工具程式為例,展示程式執行的流程,理解目前的框架是如何組成的。
main 函數的第一行呼叫了 route 函數,顧名思義就是程式本身要在這裡理解使用者究竟想要使用什麼功能
util, err := route()
...
func route() (common.Util, error) {
switch {
case strings.HasSuffix(os.Args[0], "readelf"):
util, err := readelf.Init(os.Args[len(os.Args)-1])
...
這個 route 函式的核心就是一個很大的 switch-case 結構,這裡展示出的只是 readelf 的部份。程式認定使用者意圖要執行 readelf 之後,隨即進行專屬的初始化,並傳入最後一個命令列參數,也就是要被展示的二進位檔。這個初始化 Init 函數所傳回的兩個值,第一個是 readelf 所需要的資料結構,型別是定義在 common/util.go
的 Util
介面型態。
比方說,二維形狀可以是包含週長和面積兩個計算函式的介面,每一個幾何圖形都可以有各自的資料結構,但只要定義了計算週長、計算面積的兩個函數,就都可以被當作二維形狀介面型態的變數被傳遞。詳細請參考這個例子。
回傳 util 這個型別為 Util 介面的結構後,處理命令列參數
args := util.DefineFlags()
flag.Usage = printUsage
flag.Parse()
其中,flag
是一個內建的命令列程式用的參數處理函式庫。這個 args
變數是 map 型態,由字串對應到布林、整數或字串的任一型別。在 Parse
函數呼叫之後,程式已經取得了那些參數的相關值,因此可以正式執行:
raw, err2 := util.Run(args)
回傳的字串暫且取名叫做 raw,代表還未格式化輸出的原始檔案,但是之後應該會引入 json 相關函式庫來處理。之後就可以輸出了,這個部份預計會使用 text/tabwriter
這個專門格式化輸出的函式庫,現在則還沒有什麼內容。
common.Output(raw)
可以執行看看
$ $GOPATH/bin/readelf
$ $GOPATH/bin/readelf /bin/ls
$ $GOPATH/bin/readelf -S -l -h /bin/ls
分別會出現什麼?
相關功能可以在
readelf/readelf.go
裡面找到。
今日的進度中,筆者介紹了 riscv-go 的環境架設,並且實作了一個能夠方便延伸的框架。設計上也許不夠成熟,但是要相容於 GNU 的傳統作法的話是絕對可行的,而筆者試圖引入的設計則是為鐵人賽尾聲時的專題實作預先鋪路。
到這裡已經花了三日在廣泛意義的環境架設,之後就會進入第二階段的工具程式篇。筆者將一個一個挑出感興趣的部份,並且選擇其中的部份功能來實作,最終目標是要透過這些實作來了解 ELF 檔之中的祕密。
總之,這次絕不跳票,明天就可以開始來做 readelf 啦!我們明日再會!