動態鏈接

一、靜態鏈接缺陷

靜態鏈接需要把數據和代碼都連接到自己的可執行文件中,運行時系統中有時會存在多個庫文件副本從而容易造成內存和磁盤的空間浪費、其次靜態鏈接模塊更新困難。爲了解決這兩個問題需要把程序的模塊分隔開,生成相互獨立的文件從而不在將他們鏈接在一起。對目標文件的鏈接等到運行時在進行,這就是動態鏈接。

二、動態鏈接的基本實現

Linux下的動態鏈接文件爲.so,Windows下動態鏈接文件爲.dll

1、當程序被裝載時,系統的動態鏈接器會將程序所需要的所有動態鏈接庫裝載到進程的地址空間,並且將程序中所有未決議的符號綁定到相應的動態鏈接庫中,並進行重定位工作。

2、動態庫編譯

gcc -fPIC -shared -o lib.so lib.c  即得到lib.so動態庫(-fPIC 產生地址無關代碼)

編譯.o目標文件時需要使用 -fPIC -shared參數,如:

gcc -fPIC -c lib.c 編出的.o文件

gcc -shared -o lib.so lib.o lib1.o 編出的lib.so文件纔可用於動態庫編譯。

共享庫都是通過使用附加選項 -fpic 或 -fPIC 進行編譯,從目標代碼產生位置無關的代碼

(Position Independent Code,PIC),使用 -shared選項將目標代碼放進共享目標庫中。

gcc -o p1 p1.c ./lib.so   p1.c中調用lib.so庫中函數

gcc -o p2 p2.c ./lib.so   p2.c中調用lib.so庫中函數

gcc -o p1 p1.c -L./ -l0 -l1 

使用export查看設置環境變量

​export LD_LIBRARY_PATH=/workteam/liupengyf1/data/work/deal/dynamic

export LD_LIBRARY_PATH=$(pwd)

3、動態鏈接執行步驟:加載所有有關係的動態庫至內存(動態鏈接器)->鏈接(符號解析、地址重定位)->程序開始運行​。

鏈接時如果是靜態符號,鏈接器會按照靜態規則將其鏈接到可執行文件中,如果是動態符號,那麼鏈接器會將其標記爲一個動態鏈接的符號,不對他進行地址重定位,把這個過程留到裝載時在進行,那麼鏈接器是如何知道符號是動態符號還是靜態符號的呢?這實際上就是鏈接的時候​需要指定動態庫的原因,鏈接器通過動態庫中符號的解析知道哪些符號是動態符號。

4、裝載時的重定位

共享對象在編譯時不能假設​自己在進程的虛擬地址空間中的位置,而可執行文件基本可以確定自己在進程虛擬地址空間中的位置。因爲可執行文件往往是第一個被加載的文件,Linux下一般爲0x08040000,Windows下爲0x0040000。由於動態共享對象在鏈接時沒有把對絕對地址的引用進行重定位所以系統在裝載程序的時候需要對程序的指令和數據中的絕對地址引用進行重定位。裝載時重定位是解決動態模塊中有絕對地址引用的方法之一。

靜態鏈接使用的是鏈接時重定位,動態鏈接使用的是裝載時重定位,Linux支持裝載時重定位的方法,前面生成共享對象時使用了“-share”“-fPIC”兩個參數,如果只使用“-share”生成的共享對象就使用裝載時重定位的方法。

5、地址無關代碼

但是裝載時重定位有很大的一個缺陷即指令部分無法在多個進程間共享,裝載時重定位需要修改指令所以沒有辦法做到一份指令被多個進程共享。數據部分每個進程都有自己的副本所以可以在裝載是進行重定位。前面提到的問題一種解決辦法就是把指令中需要修改的部分提取出來存放到數據段中,這樣指令部分在重定位是不需要變動可以在多進程間共享,數據部分每個進程有自己的副本,這種技術就叫做地址無關代碼。

按照引用的方式不同可以分爲:

(1)模塊內部的函數訪問

此種類型的訪問時相對地址的訪問不需要重定位。​

(2)模塊內部的數據訪問

此種類型的訪問時相對地址的訪問不需要重定位。​​

(3)模塊外部的函數訪問

(4)模塊外部的數據訪問

編譯時使用參數-fPIC或者-fpic可以生成地址無關代碼。​這兩個參數的功能一樣,唯一的區別就是-fpic產生的代碼更小,執行更快。地址無關代碼與硬件相關,不同的硬件有着不同的實現,-fpic在某些硬件平臺上有限制,例如全局符號的數量和代碼的長度等,而-fPIC沒有這種限制,所以一般使用-fPIC。

判斷共享對象文件是否有使用PIC命令:readelf -d foo.so | grep TEXTREL​,如果有任何輸出則說明沒有使用-fPIC,TEXTREL​代表代碼段重定位表地址,-fPIC的對象不會包含任何代碼段重定位表

不加fPIC編譯出來的so,是要再加載時根據加載到的位置再次重定位的,因爲它裏面的代碼並不是位置無關代碼)如果被多個應用程序共同使用,那麼它們必須每個程序維護一份so的代碼副本了,因爲so被每個程序加載的位置都不同,顯然這些重定位後的代碼也不同,當然不能共享。

​6、延遲綁定

動態鏈接使用的是裝載時重定位所以程序的性能會受到影響,爲了解決這個問題ELF採用一種叫延遲綁定的技術,基本思想就是函數第一次使用時才進行綁定(符號查找,重定位等),如果沒有用到則不進行重定位。這種技術大大加快了程序啓動速度。

7、動態鏈接器

Linux下,動態鏈接器實際是一個共享對象,操作系統通過普通共享對象的方式將其映射到進程的地址空間中,操作系統在加載完動態鏈接器之後將控制權交給動態鏈接器,動態鏈接器會開始對可執行文件進行動態鏈接工作,之後將控制權重新交給操作系統。動態鏈接器的位置既不是有系統決定也不是由環境變量決定而是由ELF文件自己決定,有一個專門的段叫做“.interp”。使用objdump命令可以查看:

objdump -s a.out

readelf -l a.out | grep interpreter

".dynamic"段中保存了依賴哪些共享對象及動態鏈接符號表的位置等,使用readelf -d 命令可以查看".dynamic"段內容。

readelf -d a.out

".dynsym"表示動態庫的導入導出符號段,只保存動態鏈接相關的符號。​

​三、動態庫的運行時加載

運行時加載是指通過調用系統函數dlopen(),dlsym(),dlerror(),dlclose()等一系列函數程序自己在運行時指定加載某個共享對象,並且在你不需要時將其卸載。

此種方式使用共享庫時由於沒有符號的導入所以在鏈接時不需要指定使用的共享庫。​

四、鏈接版本

如果系統中存在兩個相同鏈接名的動態庫和靜態庫,如:libc.a和libc.so.2.0.0那麼編譯程序時-lc會鏈接哪個呢?

ld使用“-static”參數時會鏈接libc.a,使用“-Bdynamic”(默認情況)會鏈接libc.so.2.0.0

五、共享庫的系統路徑

Linux一般都遵循FHS標準:

(1)、/lib存放系統最關鍵最基礎的共享庫,如動態鏈接器,C語言的運行庫等。

(2)、/usr/lib​存放非系統運行時所需要的關鍵共享庫。

(3)、/usr/local/lib存放一些跟操作系統本身並不十分相關的庫​

六、環境變量

Linux中使用LD_LIBRARY_PATH​環境變量可以臨時改變某個應用程序查找共享庫的路徑,不會影響系統中的其他程序。LD_LIBRARY_PATH是由若干個路徑組成的環境變量,每個路徑之間由冒號隔開,默認情況爲空。

爲某個進程設置LD_LIBRARY_PATH之後,進程啓動之後會首先查找由LD_LIBRARY_PATH指定的目錄。

動態鏈接器加載和查找共享庫的順序依次是:

(1)編譯目標代碼時指定的動態庫搜索路徑(如-rpath)

(2)由環境變量LD_LIBRARY_PATH指定的路徑

(3)由路徑緩存文件/etc/ld.so.cache指定的路徑。​

(4)默認共享目錄,先/usr/lib​然後/lib

ld.so.conf是一個文本配置文件,他可能包含其他配置文件,這些配置文件包含着路徑信息。鏈接器查找共享庫時都應該去遍歷這些目錄,但是爲了提高效率和速度Linux將所有這些路徑下的共享庫的符號鏈接集中存放到ld.so.cache文件裏面,這樣動態鏈接器查找文件時只需要在文件ld.so.cache中查找。

發佈了43 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章