這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go team 總是能帶來一些驚喜的,關於 Go 的連結器,看來在 1.3 版本中要大修了。
————翻譯分隔線————
Go 1.3 連結器大修
Russ Cox
2013 年 11 月
摘要
在構建和運行一個標準的 Go 程式時,連結器是最慢的一部分。為瞭解決這個問題,我們計劃將連結器拆分到兩部分。其中的一部分可能會用 Go 來編寫。
背景
連結器總是 Plan 9 工具鏈中最慢的部分之一,而現在它是 Go 工具鏈中最慢的部分了。Ken Thompson 在關於工具鏈的概述中進行了總結:
新的編譯器編譯迅速、載入緩慢,產生中等品質的目標代碼。編譯器與移植性相關,對於不同的電腦需要若干星期的工作來構建對應的編譯器。對於 Plan 9 來說,需要若干有特定功能、且使用自己的目標格式的編譯器,這一項目不可或缺。對於我們來說,編譯器必須可以自由的伴隨 Plan 9 發行。
在回顧中帶來了兩個問題。首先是必須對編譯器和載入器進行簡化。Plan 9 運行在多種處理器上,而這些編譯通常會並行完成。不幸的是,在載入進行之前,所有的編譯都必須完成。載入是單線程的。在這個模型下,任何用於載入的編譯結果的變化都會顯著的增加實際時間。著對於那些經常編譯和載入的庫來說同樣如此。未來,我們可能嘗試將一些載入工作放到編譯器完成。
這篇文檔編寫於上世紀 90 年代初期。現在就是未來。
建議規劃
當前的連結器執行兩個獨立的任務。首先,它伴隨定位表,將偽指令輸入資料流翻譯為可執行代碼和資料區塊。然後,它刪除無用代碼,合并其他到單一的鏡像中、重新處理定位,並且產生一些例如運行時符號表這樣的完整程式資料結構。
第一部分可以分解到一個庫(liblink)中,這樣就可以同彙編器與編譯器聯合。那些 6a、6c 或 6g 等等輸出的目標檔案可以由 liblink 輸出,且包含可執行代碼、資料區塊與定位表,即當前連結器的第一個中間產物。
第二部分可以由移除 liblink 之後的連結器處理。剩下的程式可以讀取新的目標檔案並完成連結。這個連結器只有很少的代碼,大部分是與架構無關的。這使得將其合并為一個與平台無關的程式,然後像“go tool ld”這樣調用成為可能。甚至使用 Go 來編寫它,使其在大型連結的時候更加容易並行化。(參閱下面的章節瞭解如何開始。)
一開始,我們將集中精力使新剝離的部分能用於 C 代碼。一旦所有修改完成,就開始 Go 的探索。
為了避免影響可用性,產生的目標檔案將保持已有的副檔名 .5、.6、.8。可能在 Go 1.3 中,新的連結器可以包含叫做 5l、6l 和 8l 的過渡性程式。這些過渡程式將在 Go 1.4 中移除。
目標檔案
新的拆分需要新的目標檔案格式。當前的目標檔案包含偽指令流,但是新的目標檔案將包含可執行代碼以及伴隨定位表的資料區塊。
一個自然的問題是應當使用已有的目標檔案格式嗎,例如 ELF。首先,我們將使用定製的格式。為了構建像運行時符號表這樣的運行時資料結構,需要定製一個 Go 的連結器,因此即使使用 ELF 目標檔案,也無法重用標準的 ELF 連結器。ELF 檔案也包含了大量超過 Go 定製的連結器所需要的通用語義和 ELF 語義。一個定製的,不那麼通用的目標檔案格式可以讓產生與使用更加簡單。另一方面,ELF 可以由如 readelf、objdump 等等的標準工具處理。不過,一旦塵埃落定,當我們知道到底這個格式需要什麼的時候,還是值得確認一下使用 ELF 能否滿足需求。
新的目標檔案的細節還未完成。這個章節的剩餘部分列出了一些設計的思路。
顯然,這個檔案越簡單越好。除了一些例外,連結器的工作都應當在中間庫中完成。可能會神奇地包含棧劃分代碼,這使得目標檔案是作業系統依賴的,雖然它們在包中已經是基於依賴作業系統的 Go 代碼了。同時軟體的浮點工作也在中間庫中完成,使得 ARM 目的地檔案是特定的 GOARM(當前在連結器運行之前,沒有任何東西特定是 GOARM 的)。
我們需要確保目標檔案是可以通過 mmap 使用的。這可以降低複製帶來的 I/O。這需要修改 Go 的運行時對於非 nil 的地址產生 SIGSEGV 時,產生一個 panic 而不是崩潰掉。
純 Go 包由 Go 編譯器在一個 Go 代碼檔案完整的集合上一次產生一個目標檔案。接下來這個目標檔案被封裝到一個打包檔案中。我們可以整理這個單一的目標檔案也是一個合法的打包檔案,那麼通常情況下就無需封裝的步驟。
啟動
如果新的 Go 連結器是用 Go 編寫的,就有一個啟動問題:如何連結這個連結器?這裡有兩個方案。
第一個方案是維護一個 CL 啟動列表。隊列中的第一個 CL 應當包含當前的連結器,由 C 編寫。每個步驟都是一個 CL,包含了新的連結器用來連結下一個。隊列的最後一個二進位結果就可以用於下載了。隊列不能太長,並且能貼合裡程碑。例如,可以讓 Go 1.3 的連結器可以由 Go 1.2 的程式進行編譯,Go 1.4 的連結器可以由 Go 1.3 的程式進行編譯,等等。隊列的記錄確保了如果需要,可以重新啟動,不過也得提供對付信任依賴問題的機制。另一個啟動的方法是使用 gccgo,然後使用它來編譯 Go 1.3 連結器。
第二個方案是在用 Go 寫了一個更好的連結器之後,也仍然維護 C 的版本,並且保持大多數功能對等。用 C 編寫的版本只需要保持足夠的功能來連結 Go 編寫的這個。它需要載入一些目標檔案,合并它們,然後輸出可執行檔。無需 cgo 支援、無需外部連結、無需共用庫、無需高效能。只需要一些樸素的代碼(可能只有幾千行代碼)並且不需要經常修改。C 版本會在 make.bash 的時候構建和使用,但不會被安裝。這個方案對於其他從 Go 源碼構建的開發人員來說更加容易。
我們選擇哪個方案並不重要,無論怎樣都至少會有一個可用的方案。事情繼續推進,我們就可以決定。