昨日實際利用了 go 語言函式庫取得了檔頭內的一定資訊,並且暫且使用 json 格式當作最後的輸出。當然,我們殘留一些未竟之項目,也引導出一些疑問:
Header64
結構相比少了許多項資訊,那些資訊是什麼?為什麼 debug/elf
套件忽略它們?不會出問題嗎?flag
函式庫來管理命令列參數,難道沒有什麼方法可以取得參數之外的命令列內容嗎?筆者承認這些問題拳拳到肉,原先也在心中天人交戰了一番,最後還是決定寧可自曝其短,不可欺騙世人;天下之大,總有一天會有先進看到這系列文而感到不齒,不如自己先坦承面對不足之處再修正,這樣也才符合鐵人賽「見證自己的成長、分享成長的成果」的宗旨。
那麼,在進入昨日預約的 -l
和 -S
之前,筆者這裡先正面回應這些遺留的問題。
關於這個,筆者一開始也是相當驚訝。最後採取的切入點是,直接去看函式庫內的實作究竟是什麼樣子?
這也是另外一個讓人對 go 語言死心塌地的原因:函式庫的程式碼非常容易取得!以 C 語言開發為例,通常標準函式庫仰賴一個厚重的 libc.so 也就罷了,它還只是個不可閱讀的二進位檔;而且,除非由自己編成,否則還得透過各個發行版的軟體建置方法去尋找特定版本所使用的原始碼包。go 語言則不然,標準函式庫的源碼內容,都在預設的 $GOROOT/src 目錄底下!
思考過程是這樣子的:如果 FileHeader 的資訊短少會造成讀取檔案問題,那麼是否我們在 common.Init
函數中引用的 elf.Open
其實沒有成功讀取 ELF 檔內容?筆者選擇相信 go 語言的實作,因此少了這些資訊並不影響正確性,只是這份函式庫在設計時選擇將那些資訊做了有意義的利用之後篩選掉了,也許才是真正的原因。
果然,在筆者環境中的 /lib/go/src/debug/elf/file.go
之中有 Open
函式的內容,從中不難看出一點線索。Open
先用作業系統統一的 os.Open
介面開啟了檔案之後,使用 ELF 相關的 NewFile
函式閱讀該檔案。在最初的 16 個位元組被讀取完後,
...
263 // Read ELF file header
264 var phoff int64
265 var phentsize, phnum int
266 var shoff int64
267 var shentsize, shnum, shstrndx int
268 shstrndx = -1
269 switch f.Class {
270 case ELFCLASS32:
271 hdr := new(Header32)
272 sr.Seek(0, io.SeekStart)
273 if err := binary.Read(sr, f.ByteOrder, hdr); err != nil {
274 return nil, err
275 }
276 f.Type = Type(hdr.Type)
277 f.Machine = Machine(hdr.Machine)
278 f.Entry = uint64(hdr.Entry)
279 if v := Version(hdr.Version); v != f.Version {
280 return nil, &FormatError{0, "mismatched ELF version", v}
281 }
282 phoff = int64(hdr.Phoff)
283 phentsize = int(hdr.Phentsize)
284 phnum = int(hdr.Phnum)
285 shoff = int64(hdr.Shoff)
286 shentsize = int(hdr.Shentsize)
287 shnum = int(hdr.Shnum)
288 shstrndx = int(hdr.Shstrndx)
289 case ELFCLASS64:
...
可以看到 264~268 行有著 HeaderXX 有,而 FileHeader 沒有的成員被宣告了,而在 switch-case
的條件結構(這裡只有節錄 32 位元版本)裡面可以見到,函式庫的實作的確是取用了一整組的 HeaderXX
結構體,也在 282~288 行左右陸續將這些資訊初始化。那麼為什麼會沒有回到原本的 FileHeader
結構之中呢?後面的程式碼片段就過於冗長,因此筆者在這裡揭露真相:因為,這些資訊都在完整利用了,無須存回 ELF 檔頭之中。至於所謂的完整利用是什麼意思?那就是下個章節實作 -l
與 -S
時會遇到的事情了。
關於這個,各位讀者可以回顧第三日,我們在初次展示 debug/elf
函式庫的時候有帶到 GoString 方法的引用,那就是轉換內部數字編碼而成為人類可讀的方法。
筆者原先想要讓一個共用的 Output
就能處理來自所有工具程式的可能輸出格式,因此有這樣的集中化設計。但是這樣果然會無意義的複雜化輸出函式的設計,因此這裡筆者決定把原先的結構改寫:取消共用的輸出函式,新增一個 Output
到工具程式介面之中。接下來才是排版的問題,我們這裡會使用 text/tabwriter
函式庫來解決格式化輸出的問題。
是的,這完完全全是筆者的痛點。現在的問題在於,route
函式就已經要為 readelfUtil 型別初始化完成,這項功能不能沒有目標檔案、甚至多數目標檔案的檔名;偏偏筆者又為將來擴充考量,將各個工具程式的參數定義(DefineFlag
)函式放在各個函式庫中,進而導致一個「A 不能先於 B,也不能後於 B」的窘況。
然而,這是因為筆者沒有遵守物件導向典範的原因,請看後續修正。
這兩個可以一起解決,畢竟都是顯示時的問題。在 common/util.go
中新增一行引用 text/tabwriter
函式庫(相關教學可以參考官方文件,非常詳細),然後在 Output 之中改成這麼作
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 0, '\t', 0)
if *args["h"].(*bool) {
var output elf.FileHeader
json.Unmarshal(reu.raw["h"], &output)
fmt.Fprintln(w, "ELF File Header:\t\t")
fmt.Fprintln(w, "\tClass:\t", output.Class.GoString())
fmt.Fprintln(w, "\tData:\t", output.Data.GoString())
fmt.Fprintln(w, "\tOSABI:\t", output.OSABI.GoString())
fmt.Fprintln(w, "\tABIVersion:\t", output.ABIVersion)
fmt.Fprintln(w, "\tType:\t", output.Type.GoString())
fmt.Fprintln(w, "\tMachine:\t", output.Machine.GoString())
fmt.Fprintln(w)
w.Flush()
}
這裡一口氣引用了兩種函式庫,分別是為了排版的以及 json 解碼器。簡單來說,這個程式碼區段首先定義了輸出的排版格式,之後輸出程式會以 tab
符號當作分隔,輸出三欄的格式。至於原先都是數字的內部編碼問題,可以呼叫 GoString
方法來顯示。這時候輸出結果類似這樣:
$ make && mv go-binutils /tmp/readelf && /tmp/readelf -h /bin/ls
go build
ELF File Header:
Class: elf.ELFCLASS64
Data: elf.ELFDATA2LSB
OSABI: elf.ELFOSABI_NONE
ABIVersion: 0
Type: elf.ET_DYN
Machine: elf.EM_X86_64
是不是自動對得很整齊呢?雖然相比於 GNU 的版本少了許多資訊,但筆者認為有些是不需要的內容(如版本號,等到資訊界有改版消息再說吧),有些則必須等到後面我們探討過剩下的兩種檔頭之後,才能夠補完。
原先筆者規劃給各個工具程式的初始化框架是,在 main
呼叫 route
之後,在 route
之中呼叫一個相對應的 Init
程式就宣告完成。但是這在這個情境下是不方便的寫法,因為我們需要在各個參數的樣式與意義被定義出來之後,才有辦法判定哪些命令列參數是要處理的 ELF 檔案。所以這裡我們把原先的 Init
拆分成兩個部份:
New
:這個負責回傳初始的工具程式結構。以 readelf 為例,就是 readelfUtil
。Init
:這個真正負責初始化,放置在 DefineFlags
呼叫之後以便取得所要處理的檔案。為求簡單,這裡也先取得剩下的最後一個命令列參數作為目標檔案就好,反正架構對了日後就好擴充了。改正的過程在 main
函式中最為清楚:
- raw, err2 := util.Run(args)
+ tail := flag.Args()
+ err1 := util.Init(tail[len(tail)-1])
+ if err1 != nil {
+ fmt.Println(err1.Error())
+ return
+ }
+
+ err2 := util.Run(args)
原本今天規劃是要完成 -l
與 -S
的說明與實作,但是某種程度來說這個規劃也展示了軟體開發過程中的一個特點:超出預期的時程。如果要直接切入剩下的這兩個,當然也不是不可行,但由於眼前的問題實在太過棘手,沒有辦法忽略,同時也有一些介紹的價值,於是就著手處理了一番。對於 go 語言有興趣的讀者應該可以在本日的範例中學到一些招式。
筆者也不覺得自己在偷文章。單以字數論,每日的進度都約有 3000 字左右的穩定輸出。以內容論,也盡可能涵蓋到為什麼這樣做、如何做以及做出來的成果,可以很自豪地說無愧於自己也無愧於讀者。雖然進度是慢了一點,但是這個開頭的階段,就類似於要解釋 ls
的內容一般。ls
雖然簡單也輕鬆好上手,但若是要解釋一個 ls -l
的內容,也是必須提到作業系統權限、檔案系統、使用者與群組管理等觀念,我們這個系列探討 ELF 格式更是如此。
總之感謝各位讀者的耐心等待,我們明天就可以正式來面對剩下的兩個參數,以及從高角度俯瞰 ELF 格式的剩下兩個元件:程式與區段。