《程序員的自我修養》學習筆記(六)————動態鏈接(1):爲什麼要動態鏈接

1. 靜態鏈接缺點

         靜態鏈接諸多缺點,比如浪費內存和磁盤空間、模塊更新困難等。
         內存和磁盤空間:靜態鏈接的方式對於計算機內存和磁盤的空間浪費非常嚴重,特別是在多進程操作系統情況下。每個進程都有靜態庫的備份,非常浪費內存空間。
         程序開發和發佈:空間浪費是靜態鏈接的一個問題,另一個問題是靜態鏈接對程序的更新、部署和發佈也會帶來很多麻煩。一旦程序中有任何模塊更新,整個程序就要重新鏈接、發佈給用戶。如果程序都使用靜態鏈接,那麼通過網絡來更新程序將會非常不便,因爲一旦程序任何位置的一個小改動,都會導致整個程序重新下載。

2. 動態鏈接

         要解決空間浪費和更新困難這兩個問題最簡單的辦法就是把程序的模塊相互分割開來,形成獨立的問題,而不再將它們靜態地鏈接在一起。簡單地講,就是不對那些組成程序的目標文件進行鏈接,等到程序要運行時才進行鏈接。也就是說,把鏈接這個過程推遲到了運行時再進行,這就是動態鏈接(Dynamic Linking)的基本思想。動態鏈接解決了共享的目標文件多個副本浪費磁盤和內存空間的問題。另外在內存中共享一個目標文件模塊的好處不僅僅是節省內存,它還可以減少物理頁面的換入換出,也可以增加CPU緩存的命中率,因爲不同進程間的數據和指令訪問都集中在了同一個共享模塊上。動態鏈接方案也可以使程序的升級變得更加容易,當我們要升級程序庫或程序共享的某個模塊時,理論上只要簡單地將舊的目標文件覆蓋掉,而無須將所有的程序再重新鏈接一遍。當程序下一次運行的時候,新版本的目標文件會被自動裝載到內存並且鏈接起來,程序就完成了升級的目標。動態鏈接的方式使得開發過程中各個模塊更加獨立,耦合度更小,便於不同的開發者和開發組織之間獨立進行開發和測試。

2.1 動態鏈接優點

         程序可擴展性和兼容性:動態鏈接還有一個特點就是程序在運行時可以動態地選擇加載各種程序模塊,這個優點就是後來被人們用來製作程序的插件(Plug-in)。比如某個公司開發完成了某個產品,它按照一定的規則制定好程序的接口,其它公司或開發者可以按照這種接口來編寫符合要求的動態鏈接文件。該產品程序可以動態地載入各種由第三方開發的模塊,在程序運行時動態地鏈接,實現程序功能的擴展。
        動態鏈接還可以加強程序的兼容性。一個程序在不同的平臺運行時可以動態地鏈接到由操作系統提供的動態鏈接庫,這些動態鏈接庫相當於在程序和操作系統之間增加了一箇中間層,從而消除了程序對不同平臺之間依賴的差異性。

2.2 動態鏈接的基本實現思想

        動態鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時纔將它們鏈接在一起形成一個完整的程序,而不是像靜態鏈接一樣把所有的程序模塊都鏈接成一個單獨的可執行文件。
        動態鏈接涉及運行時的鏈接及多個文件的裝載,必需要有操作系統的支持,因爲動態鏈接的情況下,進程的虛擬地址空間的分佈會比靜態鏈接情況下更爲複雜,還有一些存儲管理、內存共享、進程線程等機制在動態鏈接下也會有一些微妙的變化。目前主流的操作系統幾乎都支持動態鏈接這種方式,在Linux系統中,ELF動態鏈接文件被稱爲動態共享對象(DSO, Dynamic Shared Objects),簡稱共享對象,它們一般都是以“.so”爲擴展名的一些文件;而在Windows系統中,動態鏈接文件被稱爲動態鏈接庫(Dynamical Linking Library),它們通常就是很常見的以“.dll”爲擴展名的文件。
         在Linux中,常用的C語言庫的運行庫glibc,文件名叫做“libc.so”。整個系統只保留一份C語言庫的動態鏈接文件“libc.so”,而所有的C語言編寫的、動態鏈接的程序都可以在運行時使用它。當程序被裝載的時候,系統的動態鏈接器會將程序所需要的所有動態鏈接庫(最基本的就是libc.so)裝載到進程的地址空間,並且將程序中所有未決議的符號綁定到相應的動態鏈接庫中,並進行重定位工作。程序與libc.so之間真正的鏈接工作是由動態鏈接器完成的,而不是靜態鏈接器ld完成的。也就是說,動態鏈接是把鏈接這個過程從本來的程序裝載前被推遲到了裝載的時候。

3. 一個簡單的例子

/*Program1.c*/
#include "Lib.h"

int main(void)
{
	foobar(1);
	return 0;
}
/*Program2.c*/
#include "Lib.h"

int main(void)
{
	foobar(2);
	return 0;
}
/*Lib.c*/
#include <stdio.h>
void foobar(int i)
{
	printf("Printing from Lib.so %d\n",i);
}
/*Lib.h*/
#ifndef LIB_H
#define LIB_H

void foobar(int i);

#endif
gcc -fPIC -shared -o Lib.so Lib.c
gcc -o Program1 Program1.c ./Lib.so 
gcc -o Program2 Program2.c ./Lib.so 

         在靜態鏈接時,整個程序最終只有一個可執行文件,它是一個不可以分割的整體;但是在動態鏈接下,一個程序被分成了若干個文件,有程序的主要部分,即可執行文件(Program1)和程序所依賴的共享對象(Lib.so),很多時候我們也把這些部分稱爲模塊,即動態鏈接下可執行文件和共享對象都可以看作是程序的一個模塊。
        當程序模塊Program1.c被編譯成爲Program1.o時,編譯器還不知道foobar()函數的地址。但鏈接器將Program1.o鏈接成可執行文件時,這時候鏈接器必須確定Program1.o中所引用的foobar()函數的性質。如果foobar()是一個定義與其它靜態目標模塊中的函數,那麼鏈接器將會按照靜態鏈接的規則,將Program1.o中的foobar地址引用重定位;如果foobar()是一個定義在某個動態共享對象中的函數,那麼鏈接器就會將這個符號的引用標記爲一個動態鏈接的符號,不對它進行地址重定位,把這個過程留到裝載時再進行。鏈接器如何知道foobar的引用是一個靜態符號還是一個動態符號?這實際上就是我們要用到Lib.so的原因。Lib.so中保存了完整的符號信息(因爲運行時進行動態鏈接還須使用符號信息),把Lib.so也作爲鏈接的輸入文件之一,鏈接器在解析符號時就可以知道:foobar是一個定義在Lib.so的動態符號。這樣鏈接器就可以對foobar的引用做特殊的處理,使它成爲一個對動態符號的引用。
         動態鏈接程序運行時地址空間分佈:對於靜態鏈接的可執行文件來說,整個進程只有一個文件要被映射,那就是可執行文件本身。但是對於動態鏈接來說,除了可執行文件本身之外,還有它所依賴的共享目標文件。
         爲了查看進程的虛擬地址空間分佈,我們修改Lib.c的代碼,加一行sleep(-1)休眠指令。

/*Lib.c*/
#include <stdio.h>
void foobar(int i)
{
	printf("Printing from Lib.so %d\n",i);
	sleep(-1);
}

        結果如下圖所示,可以看到,整個進程虛擬地址空間中,多出了幾個文件的映射。Lib.so與Program1一樣,它們都是被操作系統用同樣的方法映射至進程的虛擬地址空間,只是它們佔據的虛擬地址和長度不同。   

        Program1除了使用Lib.so以外,它還用到了動態鏈接形式的C語言運行庫libc-2.10.1.so。另外還有一個很值得關注的共享對象就是ld-2.10.1.so,它實際上是Linux下的動態鏈接器。動態鏈接器與普通共享對象一樣被映射到了進程的地址空間,在系統開始運行Program1之前,首先會把控制權交給動態鏈接器,由它完成所有的動態鏈接工作以後再把控制權交給Program1,然後開始執行。

        通過readelf工具來查看Lib.so的裝載屬性,結果如下圖所示。

         除了文件的類型與普通程序不同以外,其它幾乎與普通程序一樣。還有有一點比較不同的是,動態鏈接模塊的裝載地址是從地址0x00000000開始的。我們知道這個地址是無效地址,並且從上面的進程虛擬空間分佈看到,Lib.so的最終裝載地址並不是0x00000000。從這一點我們可以推斷,共享對象的最終裝載地址在編譯時是不確定的,而是在裝載時,裝載器根據當前地址空間的空閒情況,動態分配一塊足夠大小的虛擬地址空間給相應的共享對象。

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章