1. 程式人生 > >Golang在ARM/Linux平臺上函式引數的傳遞

Golang在ARM/Linux平臺上函式引數的傳遞

一.前言

    作為一名初級的嵌入式軟體開發從業者,工作中大部分專案以C語言實現。使用C語言來編寫程式碼,通常我們可以預測到編譯生成的彙編/機器編碼的大致情況,在不同的晶片架構上,有其相應的ABI標準。而近年來逐漸流行起來的Go語言程式設計,雖然同樣語法上和C語言語法都有較為簡單的特點,也都是編譯型的靜態語言,但我們對它在基本型別——函式引數的傳遞方式就瞭解很少了。另外,Go語言的函式可以有多個返回值,其底層機制是如何實現的,也需要分析探究一下。

    本文將記錄Golang官方編譯器生成的可執行檔案在ARM/Linux平臺上函式傳遞的分析過程。測試使用的裝置是一部安卓手機,在其中安裝了Bash、Git、Vim、GDB等開源軟體(安裝過程請參考https://pan.baidu.com/s/1i5o6Lwh

中的《安裝記錄.docx》),並在C4DROID中找到了可在安卓中執行的GCC編譯器,這樣就可以在手機上編譯Golang編譯器了,其版本為1.4.3。之所以選擇該版本的Golang編譯器,是因為後續版本的Golang編譯器的編譯可能會依賴現有的Golang編譯器,這就會產生一個雞生蛋、蛋生雞的問題了。使用TELNET登入手機後,可以檢視到,現在已經可以正常工作了:


二.獲取當前的棧指標

    我們知道Go語言的函式定義的關鍵字為func,其返回值可以為空,或者有多個返回值,於是我們猜測它是以棧來傳遞引數和返回值的,當然事實確實也是如此,那麼在分析函式在呼叫瞬間、函式返回瞬間,其引數和返回值在棧空間上如何分配這個問題之前,我們需要獲取當前函式執行的棧指標。但與C語言不同,Go語言並不支援內聯彙編,那我們就需要使用其它的方法。

    這個方法也比較直接,使用匯編來實現,參照Golang程式碼庫的一些彙編格式,我們建立了一個nonsafe包,提供了一個Fetchsp()函式:


    當這個函式被引用,生成的可執行檔案中該彙編生成的指令為:


    可以看到,我們將棧指標存放在了棧指標的4位元組偏移處,這樣的就隱含了我們已知函式返回值的方法,後面我們會對其進一步驗證。另外,RET偽指令被編譯為add pc, lr, #0,可見Golang 1.4.3版本的ARM彙編器仍不完善。

三.簡單函式的引數傳遞和返回值

    有了當前函式的棧指標後,我們就可以直接把函式在呼叫前後的棧上面的資料使用fmt包中的類printf函式輸出查看了。相應的程式碼為:


    直接編譯並執行,可以得到結果:


    雖然如此,我們不能確定Fetchsp()返回的棧針是正確的,這就需要對生成的可執行檔案進一步檢視,得到在呼叫Fetchsp()之前的指令地址:


    上圖中高亮的部分是我們感興趣的地方。使用gdb除錯,讓它在呼叫Fetchsp()之前停下來:


    這樣,我們就可以確定,Fetchsp函式在Golang 1.4.3版本的正確性了。通過對執行結果的仔細分析,可以得出其下結論:

    函式在傳遞引數時,在當前函式棧指標4位元組偏移處開始傳參,[sp + 0x4]為函式第一個引數(0x7e0 = 2016),[sp + 0x8]為函式第二個引數(0x7d9 = 2009);當函式返回時,返回引數存放在最後一個引數之後的位置,即[sp+ 0xc](0x3ee = 2016 * 2009 / (2016 + 2009) = 1006)。

四.interface{}作為函式引數的傳遞

    Go語言中的函式也支援多個、不確定的引數傳遞,在《Programming In Go》一書的5.2.2.2一節中提到了型別檢測,修改過後的函式如下:


    可以看到,函式classifier()除了第一個引數型別是確定的之外,其他引數都是型別和個數不確定的。在C語言中,也有類似的函式,如printf(const char *, …)等。但是我們知道,諸如printf()之類的函式,可變引數部分基本上都是壓入棧的,而且沒有型別資訊,其型別是根據格式化字串來推測的。而Go語言則支援型別檢測,其實現的機制是怎樣的呢?

    通過對生成的可執行檔案分析,可以確定在呼叫classifier()函式之前,入棧了四個引數或者說,入棧了四個值:


    除了第一個指標函式外,那麼可以說只有三個值用於interface{}的多個引數傳遞了。於是我們猜測這三個值類似於一個結構體,包含了所有的可變引數的型別資訊和其相應的值。經過測試,我們使用下面的程式碼將其輸出:


    這一段程式碼比較難看,並不是因為寫的很複雜,而是因為使用了很多的“強制型別轉換”。在Go語言中,指標不參與數學運算,只好將其轉化為uintptr型別,再轉化為指標並對其解引用。先來看一下執行的結果:


    從上圖可以看到,上面解構造的解析可變引數的功能可以得到可變引數的資訊,與型別檢測得到的引數值很相近,這就說明了上面的解析是正確的。那麼,可變引數列表的資訊結構是怎樣的呢?

    可以確認,一個可變引數列表在棧上傳遞會有三個值,其中後兩個值很可能是相同的,表示為可變引數的個數(在例子中我們傳入了6個可變引數),而第一個值指向了一段棧空間,其中會有兩倍於可變引數的資料,每兩個為一組,其中第一個值為一個指標或標識,用於確定該引數的型別;第二個值為該引數的指標,同樣的,該指標指向棧空間,此處存放了該引數的值。其中有一個例外是nil,它的型別和指標都是0。為了進一步確認這一點,可以檢視最後一個引數,float32(2016),使用MATLAB可以驗證其十六進位制的表示為0x44fc0000:


五.總結

    瞭解Golang語言編譯出的可執行檔案的函式傳參機制,可能對學習Go語言的幫助並不大。另外,這些函式傳參的機制在GCCGO編譯生成的可執行檔案中並不存在。儘管如此,瞭解一些Golang底層的實現細節,仍然是十分有趣的,也可以略微滿足一下我們對Golang的"ABI"探究的心理。