作為一名C/C++程序員,對於編譯鏈接的過程要了然於胸。首先大概介紹一下,編譯分為3步,首先對源文件進行預處理,這個過程主要是處理一些#號定義的命令或語句(如宏、#include、預編譯指令#ifdef等),生成*.i文件;然后進行編譯,這個過程主要是進行詞法分析、語法分析和語義分析等,生成*.s的匯編文件;最后進行匯編,這個過程比較簡單,就是將對應的匯編指令翻譯成機器指令,生成可重定位的二進制目標文件。以上就是編譯的過程,下面主要介紹兩種鏈接方式--靜態鏈接和動態鏈接。
靜態鏈接和動態鏈接兩者最大的區別就在於鏈接的時機不一樣,靜態鏈接是在形成可執行程序前,而動態鏈接的進行則是在程序執行時,下面來詳細介紹這兩種鏈接方式。
一、靜態鏈接
1.為什么要進行靜態鏈接
在我們的實際開發中,不可能將所有代碼放在一個源文件中,所以會出現多個源文件,而且多個源文件之間不是獨立的,而會存在多種依賴關系,如一個源文件可能要調用另一個源文件中定義的函數,但是每個源文件都是獨立編譯的,即每個*.c文件會形成一個*.o文件,為了滿足前面說的依賴關系,則需要將這些源文件產生的目標文件進行鏈接,從而形成一個可以執行的程序。這個鏈接的過程就是靜態鏈接
2.靜態鏈接的原理
由很多目標文件進行鏈接形成的是靜態庫,反之靜態庫也可以簡單地看成是一組目標文件的集合,即很多目標文件經過壓縮打包后形成的一個文件,如下圖,使用ar命令的-a參數查看靜態庫的組成:
這里的*.o目標文件在我前面的博客《從編寫源代碼到程序在內存中運行的全過程解析》中已經講的很清楚了,不清楚的可以看一下。
以下面這個圖來簡單說明一下從靜態鏈接到可執行文件的過程,根據在源文件中包含的頭文件和程序中使用到的庫函數,如stdio.h中定義的printf()函數,在libc.a中找到目標文件printf.o(這里暫且不考慮printf()函數的依賴關系),然后將這個目標文件和我們hello.o這個文件進行鏈接形成我們的可執行文件。
這里有一個小問題,就是從上面的圖中可以看到靜態運行庫里面的一個目標文件只包含一個函數,如libc.a里面的printf.o只有printf()函數,strlen.o里面只有strlen()函數。
我們知道,鏈接器在鏈接靜態鏈接庫的時候是以目標文件為單位的。比如我們引用了靜態庫中的printf()函數,那么鏈接器就會把庫中包含printf()函數的那個目標文件鏈接進來,如果很多函數都放在一個目標文件中,很可能很多沒用的函數都被一起鏈接進了輸出結果中。由於運行庫有成百上千個函數,數量非常龐大,每個函數獨立地放在一個目標文件中可以盡量減少空間的浪費,那些沒有被用到的目標文件就不要鏈接到最終的輸出文件中。
3.靜態鏈接的優缺點
靜態鏈接的缺點很明顯,一是浪費空間,因為每個可執行程序中對所有需要的目標文件都要有一份副本,所以如果多個程序對同一個目標文件都有依賴,如多個程序中都調用了printf()函數,則這多個程序中都含有printf.o,所以同一個目標文件都在內存存在多個副本;另一方面就是更新比較困難,因為每當庫函數的代碼修改了,這個時候就需要重新進行編譯鏈接形成可執行程序。但是靜態鏈接的優點就是,在可執行程序中已經具備了所有執行程序所需要的任何東西,在執行的時候運行速度快。
問題:
二、動態鏈接
1.為什么會出現動態鏈接
動態鏈接出現的原因就是為了解決靜態鏈接中提到的兩個問題,一方面是空間浪費,另外一方面是更新困難。下面介紹一下如何解決這兩個問題。
2.動態鏈接的原理
動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時才將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有程序模塊都鏈接成一個單獨的可執行文件。下面簡單介紹動態鏈接的過程:
假設現在有兩個程序program1.o和program2.o,這兩者共用同一個庫lib.o,假設首先運行程序program1,系統首先加載program1.o,當系統發現program1.o中用到了lib.o,即program1.o依賴於lib.o,那么系統接着加載lib.o,如果program1.o和lib.o還依賴於其他目標文件,則依次全部加載到內存中。當program2運行時,同樣的加載program2.o,然后發現program2.o依賴於lib.o,但是此時lib.o已經存在於內存中,這個時候就不再進行重新加載,而是將內存中已經存在的lib.o映射到program2的虛擬地址空間中,從而進行鏈接(這個鏈接過程和靜態鏈接類似)形成可執行程序。
3.動態鏈接的優缺點
動態鏈接的優點顯而易見,就是即使需要每個程序都依賴同一個庫,但是該庫不會像靜態鏈接那樣在內存中存在多分,副本,而是這多個程序在執行時共享同一份副本;另一個優點是,更新也比較方便,更新時只需要替換原來的目標文件,而無需將所有的程序再重新鏈接一遍。當程序下一次運行時,新版本的目標文件會被自動加載到內存並且鏈接起來,程序就完成了升級的目標。但是動態鏈接也是有缺點的,因為把鏈接推遲到了程序運行時,所以每次執行程序都需要進行鏈接,所以性能會有一定損失。
據估算,動態鏈接和靜態鏈接相比,性能損失大約在5%以下。經過實踐證明,這點性能損失用來換區程序在空間上的節省和程序構建和升級時的靈活性是值得的。
4.動態鏈接地址是如何重定位的呢?
前面我們講過靜態鏈接時地址的重定位,那我們現在就在想動態鏈接的地址又是如何重定位的呢?雖然動態鏈接把鏈接過程推遲到了程序運行時,但是在形成可執行文件時(注意形成可執行文件和執行程序是兩個概念),還是需要用到動態鏈接庫。比如我們在形成可執行程序時,發現引用了一個外部的函數,此時會檢查動態鏈接庫,發現這個函數名是一個動態鏈接符號,此時可執行程序就不對這個符號進行重定位,而把這個過程留到裝載時再進行。