iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 3
0

前情提要


昨日解析了整個環境建置流程,也簡單介紹了其中元件扮演的角色。今天我們則要開始使用這個環境來實戰了!

建立 go 語言環境


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 環境中操作的結果。若有自行建置環境的讀者,請自行處理路徑的連結。

Golang ELF library


再來,我們終於要把系列標題中的最後一塊拼圖納入進度了,也就是 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 語言專案的觀念與作法


我們在寫程式時,總是得之於人者太多,所以幾乎每一種能夠實用的程式語言都有引用他人函式庫的功能。Go 語言就是昨日也曾看過的 import 保留字,但由於我們使用的只有 fmtdebug/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 需要的 commonreadelf 函式庫依賴那個 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/ 之下

go-binutils


設計理念

GNU 的 binutils 專案和 Unix 工具程式(utility)哲學基本上類同,都是希望一個工具程式本身不要太大、太多功能,而又希望他們組合起來可以靈活組合運用。其中各個工具至今都已經累積了許多實戰經驗,在各種軟體開發的場合被廣大群眾所使用。這些工具程式除了標準的 C 函式庫,依賴的還有一個 bfd 函式庫負責處理組合語言、架構相依的底層實作,是 binutils 和 gdb 的重要組成之一。

有別於傳統的作法,筆者不打算採取這樣的架構設計。理由是,go 語言一般來說為了部署的速度,生成的二進位檔預設都是靜態連結的。如果採取上述的設計,靜態連結的結果,這些工具程式整體佔用的空間將會比 GNU 的空間還要多花很多倍。雖然現在硬碟空間沒有那麼昂貴,但是佔用空間這件事情想到仍然令人不甚愉快。

彷彿聽到各位在反問,「那麼,你又有什麼高見?共用的關鍵部份使用函式庫,各個工具程式分別釋出,就算這不是唯一作法,也絕對是標準作法吧!」是的,還有一種 busybox 模式,也就是一個單一的程式,隨著自己被呼叫的方式不同,而表現出涵蓋 sh, core-utils, 系統工具、網路工具等不同的程式的行為。有興趣的讀者可以檢驗一下我們之前建立好的開發環境的 /bin 資料夾,就會發現各式各樣的工具程式如今都成了軟連結,連到唯一的可執行檔,busybox。

之所以可以作到這點,是因為利用了第 0 個命令列參數,也就是指令名稱本身,讓程式行為隨著這個名稱而有所不同。

而這正是筆者打算採取的設計模式,目前仍然只有骨架而已,但是為了未來的擴充,筆者也是使出渾身解數,當作一個架構規劃的練習。

架構解析

整體來說,go-binutils 專案的架構預計分為三個部份:

  1. main.go:這是進入點,程式在這裡解析參數,決定要呼叫哪一個工具程式,統整輸出。
  2. common/, 以及預計未來加入的函式庫:共用的 helper、界面、架構相依函式等會被各個工具程式利用的東西。
  3. readelf, ...:工具程式。

我們可以在 $GOPATH/bin 之下得到 make install 指令的產出,目前有以下三個可以執行的檔案(其中後兩者是軟連結):

  • go-binutils:唯一的二進位檔。直接執行的話,只會印出預設的使用訊息。
  • readelf:目前已經完成了初始化、解析參數、執行內容的框架,但是內容尚待完成。
  • objdump:預計未來必然會支援的工具程式之一,但是此時尚未加入,因此會被視為未定義用法。

以下各節,我們就以 readelf 工具程式為例,展示程式執行的流程,理解目前的框架是如何組成的。

realelf 的開始

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.goUtil 介面型態。

比方說,二維形狀可以是包含週長和面積兩個計算函式的介面,每一個幾何圖形都可以有各自的資料結構,但只要定義了計算週長、計算面積的兩個函數,就都可以被當作二維形狀介面型態的變數被傳遞。詳細請參考這個例子

後半段

回傳 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 啦!我們明日再會!


上一篇
第二日:環境架設及過程解析
下一篇
第四日:readelf 動工!
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30

尚未有邦友留言

立即登入留言