編譯一個c或c++程序的時候,總是使用gcc命令。gcc其實是根據不同的參數去調用預編譯器ccl,匯編器as,鏈接器ld。預編譯器ccl將源代碼編譯成匯編代碼,匯編器as將匯編代碼轉成機器指令,生成目標文件,鏈接器ld將目標文件連接成可執行文件。
匯編器as已經將匯編代碼轉成可執行的機器指令了,為什麼還要連接呢?當開發項目的時候,可能會有很多人協作,可能有很多功能獨立的模塊。一般的做法是將獨立的模塊分開實現,獨立編譯,最後再將不同的模塊組裝起來。這時需要一個可以將不同模塊代碼組裝成一個完整的可執行文件的工具,這個工具就是鏈接器ld,而這個組裝的過程就是鏈接。
在了解連接之前,我們先來看看目標文件的結構,看看目標文件中有什麼內容,是如何組織的。目標文件是一種二進制文件,以段的形式組織。在目標文件最開始處是一個固定長度的文件頭,文件頭記錄了該文件的基本信息,包括該文件的入口地址、段表的位置、段表的長度等。通過文件頭,可以找到段表,通過段表,可以找到文件中所有的段,從而遍歷整個文件。文件中主要的段有代碼段和數據段,分表存放文件的執行命令和數據。
文件頭
段表
代碼段
數據段
符號表
在目標文件中有一種段叫做符號表,符號表中記錄了定義在本目標文件中的全局符號和本目標文件中引用的全局符號。全局符號指的是全局函數或全局變量。在鏈接過程中,通過符號表,可以找到所有不在本模塊的全局符號,通過全局符號將不同的模塊組織起來。
靜態鏈接的方式很浪費計算機內存和磁盤空間。幾乎每個程序都使用到printf()、scanf()等公用庫函數,有多少個進程在運行,就意味著有多少份printf()、scanf()的代碼存放在內存中;有多少個程序就有多少份printf()、scanf()的代碼存放在硬盤中,這造成系統資源的浪費。靜態鏈接對程序的更新、部署、發布帶來了很多的麻煩。一旦程序中有任何模塊更新了,整個程序需要重新鏈接,然後將整個程序發布給用戶。
要解決空間浪費和更新困難這兩個問題,最簡單的方法就是將程序的模塊相互分割開,等到程序運行時才進行鏈接,這就是動態鏈接的基本思想。
在動態鏈接中,鏈接器ld與普通共享對象一樣被映射到進程的地址空間,在系統開始運行程序時,系統先將控制權交給鏈接器ld,等完成所有鏈接工作之後再將控制權交給進程。