iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 5
1
Software Development

與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包系列 第 5

第五日:readelf 開發過程之疑難排解

前情提要


昨日實際利用了 go 語言函式庫取得了檔頭內的一定資訊,並且暫且使用 json 格式當作最後的輸出。當然,我們殘留一些未竟之項目,也引導出一些疑問:

  • 資訊短少問題:Json 之中的內容與 Header64 結構相比少了許多項資訊,那些資訊是什麼?為什麼 debug/elf 套件忽略它們?不會出問題嗎?
  • 內部編碼問題:Json 結構的內容只輸出數字,這個要怎麼轉換回有意義的內容?
  • 顯示排版問題:是否能夠把 json 檔的原始格式改成其他方法輸出?Go 語言能夠方便的做到這件事情嗎?
  • 程式架構問題:在 main.go 裡面只把命令列最後一個參數當作目標檔案來初始化,非常不合適,既然之前使用 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 拆分成兩個部份:

  1. New:這個負責回傳初始的工具程式結構。以 readelf 為例,就是 readelfUtil
  2. 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 格式的剩下兩個元件:程式與區段。


上一篇
第四日:readelf 動工!
下一篇
第六日:收尾 readelf 工具
系列文
與妖精共舞:在 RISC-V 架構上使用 GO 語言實作 binutils 工具包30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言