鏈接器是構建和運行一個標准Go程序中最慢的一部分。為了闡明這一點,我們打算將連接器分為兩部分。也許每一部分都可以寫入GO中。
鏈接器一直是Plan9工具鏈中最慢的一部分,現在它也是Go工具鏈中最慢的一部分,肯 湯姆森的工具鏈概述總結道:
新的編譯器編譯很快速,載入很慢,產生中等質量的編譯代碼。現在編譯器都相對具有移植性,兩周左右的時間就可以構建出一個針對特殊機器的編譯器。對於Plan9,我們需要幾個專門特性和我們自己對象格式的編譯器,這個工作是獨立的。自由分配在Plan9中的編譯器也是非常重要的。
兩個問題在回顧中被提出,第一個是必須明確區分編譯器和加載器的工作,Plan9運行在多處理器上面,多是並行編譯,不幸的是,所有編譯工作必須在載入前都完成,載入工作卻是單線程的。對於這個過程模型,任何從編譯端的工作移動到載入段,這都會導致實際時間的顯著的增加。此外對於一些不需要經常編譯卻需要頻繁載入的庫來說也是同樣的成立的。所以在未來,我們會努力將一些載入工作放到編譯時完成。
這篇文檔是在90年代初完成的,未來就在這裡。
當前的鏈接器執行兩個獨立的任務。首先,它把輸入流中的偽指令翻譯成可執行的代碼和數據塊,和重定位的地址列表。 其次,它刪除無用的代碼,把余下的代碼合並到一個單獨的文件,更新重定位地址,生成幾個全局的數據結構,比如運行時符號表。
第一部分的工作可以封裝成一個庫-liblink-它可以鏈接匯編解釋器和編譯器。無論是6a,6c還是6g生成的文件,最終都會被liblink寫入包含了可執行代碼、數據塊和重定位地址表的目標文件,這是當前的鏈接器的前半部分工作的成果。
可以被處理的第二部分,那個剩余的程序會讀入新的目標文件和完成鏈接。那個鏈接器只有少量的實現代碼,而且多數是架構獨立的。那麼可以做到將這個鏈接器融入到一個單獨的架構獨立的在程序,這個程序以“go tool id”被調用。甚至有可能直接在Go中重寫鏈接器,使得並行處理大量鏈接更為容易。(參見下面的如何去引導)
開始,我們用C的時候會關注於獲得新的切片,使用Go語言的探索只會在余下的改變都完成的情況下發生。
為了避免在工具使用上的混淆,目標文件會保持現有的後綴.5 .6 .8。或許在Go1.3中,我們甚至會包含進以5l,6l,8l命名的中間程序,這些中間程序會調用新的鏈接器,但會在Go 4.1中棄用
新的分割需要一種新的目標文件格式。目前的目標文件只包含偽指令流,但是新的目標文件會包含可執行代碼和數據塊,以及重定向列表。
我們自然會問:是否應該采用一種現行的目標文件格式,如ELF?首先,我們會采用一種自定義格式。一個定制的Go鏈接器需要構建運行時數據結構,如:符號表。因此,即便是我們使用了ELF目標文件格式,我們可能也不會使用標准的ELF鏈接器。ELF文件過於通用,而且其語義過於復雜,遠遠超過定制的Go語言鏈接器的需要。一個自定義,不太通用的目標文件格式應該容易被產生和使用。另一方面,ELF能被標准工具加工處理,如:readelf,objdump等等。雖然,已經塵埃落定,我們也明確知道我們需要什麼樣的格式,不過,探討一下是否應該使用ELF還是非常值得的。
新的目標文件格式的細節還沒有設計完成。這節余下部分列出一些設計的注意事項
如果一個新的Go連接器是使用Go語言寫的,這就產生了一個自舉問題:如何連接一個連接器?這裡有兩種方法。
第一種是維護一個CL的自舉列表。序列中第一個CL應該說明當前的linker,用C寫成。後續的CL應該包含一個新的可以使用之前的連接器連接的連接器。從這些序列中產生的最終二進制文件可以被下載。這些序列必須不能太長,並且和裡程碑保持一致。例如,我們安排Go 1.3的連接器可以被當作GO 1.2的程序編譯,Go 1.4的連接器可以被當作Go 1.3的程序編譯,以此類推。被記錄的序列使得當需要時重新自舉成為可能,並且提供了一種方法來解決Trusting Trust problem問題。另一種方法是編譯gccgo 並且用它來當作Go 1.3的連接器。
第二種方法是保留一個C連接器即使我們用Go語言寫了一個更好的,並且保持兩者功能基本相同。用C寫的版本僅僅需要保留那些連接Go寫的連接器的所需要的功能。它需要揀出一些對象文件,合並他們,並長處一個可執行文件。這裡不需要cgo的支持,不需要額外的連接,不需要共享的類庫,也不必考慮性能問題。它應該只是少量的代碼(大約幾千行)並且不需要經常改動。C版本連接器會在make .bash時被構建但是不需要安裝它。這種方法使得其他的開發者從源代碼構建Go時更加容易。
不用太過關注我們使用哪種方法,僅僅知道其中的一種就可以了。我們可以等到將來再決定。