動態鏈接 要解決空間浪費和更新困難這兩個問題最簡單的辦法就是把程式的模塊相互分割開來,形成獨立的文件,而不再將它們靜態地鏈接在一起。簡單地講,就是不對那些組成程式的目標文件進行鏈接,等到程式要運行時才進行鏈接。也就是說,把鏈接這個過程推遲到了運行時再進行,這就是動態鏈接( Dynamic Linki ...
動態鏈接
要解決空間浪費和更新困難這兩個問題最簡單的辦法就是把程式的模塊相互分割開來,形成獨立的文件,而不再將它們靜態地鏈接在一起。簡單地講,就是不對那些組成程式的目標文件進行鏈接,等到程式要運行時才進行鏈接。也就是說,把鏈接這個過程推遲到了運行時再進行,這就是動態鏈接( Dynamic Linking)的基本思想。
動態庫的基本實現
動態鏈接的基本思想是把程式按照模塊拆分成各個相對獨立部分,在程式運行時才將它們鏈接在一起形成一個完整的程式,而不是像靜態鏈接一樣把所有的程式模塊都鏈接成一個個單獨的可執行文件。那麼我們能不能按照前面例子中所描述的那樣,直接使用目標文件進行動態鏈接呢?這個問題的答案是:理論上是可行的,但實際上動態鏈接的實現方案與直接使用目標文件稍有差別。我們將在後面分析目標文件和動態鏈接文件的區別。
動態鏈接涉及運行時的鏈接及多個文件的裝載,必需要有操作系統的支持,因為動態鏈接的情況下,進程的虛擬地址空間的分佈會比靜態鏈接情況下更為複雜,還有一些存儲管理、記憶體共用、進程線程等機制在動態鏈接下也會有一些微妙的變化。目前主流的操作系統幾乎都支持動態鏈接這種方式,在 Linux系統中,ELF動態鏈接文件被稱為動態共用對象(DSO, Dynamic Shared objects),簡稱共用對象,它們一般都是以“so”為擴展名的一些文件; 而在 Windows系統中,動態鏈接文件被稱為動態鏈接庫( Dynamical Linking Library),它們通常就是我們平時很常見的以“dll”為擴展名的文件。
從本質上講,普通可執行程式和動態鏈接庫中都包含指令和數據,這一點沒有區別。在使用動態鏈接庫的情況下,程式本身被分為了程式主要模塊( Progran1)和動態鏈接庫( Lib. so),但實際上它們都可以看作是整個程式的一個模塊,所以當我們提到程式模塊時可以指程式主模塊也可以指動態鏈接庫。
在 Linux中,常用的C語言庫的運行庫glib,它的動態鏈接形式的版本保存在“/lib”目錄下,文件名叫做“ libc.so”。整個系統只保留一份C語言庫的動態鏈接文件“libc.so”,而所有的C語言編寫的、動態鏈接的程式都可以在運行時使用它。當程式被裝載的時候,系統的動態鏈接器會將程式所需要的所有動態鏈接庫(最基本的就是libc.so)裝載到進程的地址空間,並且將程式中所有未決議的符號綁定到相應的動態鏈接庫中,併進行重定位工作。
程式與libc.so之間真正的鏈接工作是由動態鏈接器完成的,而不是由我們前面看到過的靜態鏈接器ld完成的。也就是說,動態鏈接是把鏈接這個過程從本來的程式裝載前被推遲到了裝載的時候。可能有人會問,這樣的做法的確很靈活,但是程式每次被裝載時都要進行重新進行鏈接,是不是很慢?的確,動態鏈接會導致程式在性能的一些損失,但是對動態鏈接的鏈接過程可以進行優化,比如我們後面要介紹的延遲綁定( Lazy Binding)等方法,可以使得動態鏈接的性能損失儘可能地減小。據估算,動態鏈接與靜態鏈接相比,性能損失大約在5%以下。當然經過實踐的證明,這點性能損失用來換取程式在空間上的節省和程式構建和升級時的靈活性,是相當值得的。
動態鏈接程式運行時地址空間分佈
對於靜態鏈接的可執行文件來說,整個進程只有一個文件要被映射,那就是可執行文件本身,我們在前面的章節己經介紹了靜態鏈接下的進程虛擬地址空間的分佈。但是對於動態鏈接來說,除了可執行文件本身之外,還有它所依賴的共用目標文件。那麼這種情況下,進程的地址空間分佈又會怎樣呢?
我們還是以上面的 Program1為例,但是當我們試圖運行 Program1並且查看它的進程空間分佈時,程式一運行就結束了。所以我們得對程式做適當的修改,在Libc中的 foobar.c 函數裡面加入sleep函數
然後就可以查看進程的虛擬地址空間分佈:
我們看到,整個進程虛擬地址空間中,多出了幾個文件的映射。Lib.so 與 Program1 一樣,它們都是被操作系統用同樣的方法映射至進程的虛擬地址空間,只是它們占據的虛擬地址和長度不同。 ProgramI除了使用Lb.so以外,它還用到了動態鏈接形式的C語言運行庫libc-2.61so。另外還有一個很值得關註的共用對象就是ld-2.6so,它實際上是 Linux下的動態鏈接器。動態鏈接器與普通共用對象一樣被映射到了進程的地址空間,在系統開始運行Program1之前,首先會把控制權交給動態鏈接器,由它完成所有的動態鏈接工作以後再把控制權交給 Program1,然後開始執行。
我們通過 readelf工具來查看 Lib. so的裝載屬性,就如我們在前面查看普通程式一樣:
除了文件的類型與普通程式不同以外,其他幾乎與普通程式一樣。還有有一點比較不同的是,動態鏈接模塊的裝載地址是從地址0x00000始的。我們知道這個地址是無效地址,並且從上面的進程虛擬空間分佈看到, Lib. so的最終裝載地址並不是0x000000是0xb7efc000。從這點我們可以推斷,共用對象的最終裝載地址在編譯時是不確定的,而是在裝載時,裝載器根據當前地址空間的空閑情況,動態分配一塊足夠大小的虛擬地址空間給相應的共用對象。
當然,這僅僅是一個推斷,至於為什麼要這樣做,為什麼不將每個共用對象在進程中的地址固定,或者在真正的系統中是怎麼運作的,我們將在下一節進行解釋。