Go語言內幕(4):目標檔案和函式後設資料

yhx發表於2015-10-06

今天,我們來看一下 Func 結構體,還會討論一些關於 Go 垃圾回收的一些細節。

本文是 《Go語言內幕(3):連結器、連結器、重定位》的後續,我會使用相同的示例程式。因此,如果你沒有讀過前面這篇部落格,我強烈去讀一下再來閱讀這篇部落格。

函式後設資料的結構體

重定位背後的原理在本系列的第三部分就已經講得很清楚了。接下來,我們來看一下 main 方法中的 Func 結構體:

你可以認為這個結構體就是由編譯器在目標檔案中建立的函式後設資料。在 Go 執行時(rumtime)中會用到這個資料結構。這篇文章解釋了 Func 結構體中不同域的格式以及其含義。在這兒,我主要分析執行時是如何使用這個後設資料的。

在執行時包內,這個結構體被對映為如下的結構體:

你可以看到並不是目標檔案中的所有資訊都被直接對映過來。一些域只是為了提供給連結器使用。同樣,這其中最有意思的是 pcsp、pcfile 與 pln。在程式計數器(program counter)被轉換成棧指標、檔名、以及行號時會分別用到這三個域。

例如,在發生 panic 時,這種轉換很有必要的。這時候,執行時只知道觸發 panic 的彙編指令的程式計數器值。所以,所以執行時需要通過計數器獲得當前檔案、當前行以及整個棧軌跡。檔案和行號可以直接使用 pcfile 與 pcln 獲得。棧軌跡則使用 pcsp 遞規獲得。

如果我們已經有了程式計數器值,問題就變成了我們怎麼取得相應的行號呢?要回答這個問題,你需要去看一下彙編程式碼並搞明白行號是如何存在目標檔案中的:

我們可以看到 26 到 38 的程式計數器包含了相應的行號 4。計數器從 39 一直到 next_program_counter  — 對應於行 5。為了儲存的效率,使用下面的對映就可以儲存這些資訊了:

實際上編譯器就是這麼做的。 pcln 域指向在 map 中一個偏移位置,這個位置處儲存了當前函式起始程式計數器值。知道偏移值和下一個函式的起始程式計數器的偏移值後,執行時就可以用二分查詢的方式找到給定程式計數器對應的行號了。

在 Go 語言中, 這種想法還是挺常見的。不僅僅是行號或者棧指標可以直接對映到程式計數器,任何整數值都可以使用 PCDATA 指令來做這樣的對映。每一次連結器發現下面這樣的指令時:

它並不會生成任何彙編指令。而是將程式計數器與第二個引數一起儲存到一個對映中,其中第一個參數列明了使用哪一個對映。通過其第一個引數,我們很方便就可以新增一個新的對映,這也就意味著對映對於編譯器和執行時是可見的,對連結器卻是透明的。

垃圾收集器(GC)是如何使用函式後設資料的呢?

函式後設資料中最後一個值得分析的就是 FuncData 陣列。這個陣列儲存了垃圾收集器所必需的資訊。Go 語言使用標記-清除mark-and-sweep)垃圾收集器。這種垃圾收集器分為兩個階段的工作。第一階段為標記階段,GC 遍歷所有仍然在使用的物件,並將其標記為可達。第二階段為清除階段,所有沒有被標記的物件都在該階段被刪除。

垃圾收集器從幾個位置處搜尋可達的物件,包括全域性變數、暫存器、棧幀以及可達物件內的指標。如果你細想一下,在棧中搜尋指標並不是一個簡單的工作。因為在執行時執行垃圾收集過程時,它到底是如何區分棧中的變數是一個指標還是非指標型別呢?這就需要 FuncData 來發揮作用了。

對於每個函式,編譯器都會為其建立兩個點陣圖向量。其中一個表示函式的引數的範圍。另一個則表示棧幀中儲存區域性變數的區域。這兩個點陣圖變數可以告訴垃圾收集器棧幀中哪些位置上是指標,這些資訊就可以幫助垃圾收集器完成垃圾收集工作了。

值得一提的是,和 PCDATA 類似,FUNCDATA 也是由 Go 偽彙編指令生成的:

這條指令的第一個引數指明這是引數區的函式資料還是區域性變數區的函式資料。每二個引數實際上是一個引用,它引用了一個儲存 GC 掩碼(mask)的隱藏變數。

下一篇

在接下來的文章中, 我會講解 Go 的自舉(bootstrap)過程。這是理解 Go 執行時的關鍵所在。下週見。

相關文章