動態(tài)鏈接
為什么需要動態(tài)鏈接
靜態(tài)鏈接使得不同的程序開發(fā)者和部門能夠相對獨立的開發(fā)和測試自己的程序模塊绰播,從某種意義上來講大大促進了程序開發(fā)的效率,原先現(xiàn)在程序規(guī)模也隨之擴大。
但靜態(tài)鏈接的缺點也暴露出來:浪費內(nèi)存、磁盤空間倔韭、模塊更新困難。
內(nèi)存與磁盤空間
靜態(tài)鏈接在計算機早期還是比較流行的瓢对,但是到了后面寿酌,其缺點也非常明顯。比如浪費內(nèi)存和磁盤空間硕蛹,更新模塊困難等醇疼。
舉個例子,每個程序內(nèi)部除了都保留了printf()法焰、scanf()等這樣的公共函數(shù)庫秧荆,還有相當一部分的其他函數(shù)庫及輔助數(shù)據(jù)結(jié)構(gòu)都會包含在其中。現(xiàn)在Linux中埃仪,一個程序用到C語言靜態(tài)庫至少1MB以上乙濒,那么100個程序就會浪費掉100MB空間。
圖示:
Program1 & Program2分別包含Program1.o和Program2.o兩個模塊卵蛉,并且還共用了Lib.o這個模塊琉兜。靜態(tài)鏈接下,P1和P2都用到了Lib.o這模塊毙玻,所以它們同時在鏈接輸出的可執(zhí)行文件P1和P2有兩個副本善涨,當同時運行兩個程序,Lib.o在磁盤和內(nèi)存中都有兩個副本娶耍,浪費空間颠放。
程序開發(fā)與發(fā)布
靜態(tài)鏈接另一個問題是對程序的更新,部署和發(fā)布也會很麻煩运准,如程序P1使用的Lib.o是由一個第三方A提供的幌氮,那么A更新Lib.o時候,P1的廠商需要拿到最新版的Lib.o胁澳,然后將P1與其鏈接后该互,將新的P1整個發(fā)布給用戶,這么做有很明顯的缺點:
程序有任何模塊更新韭畸,整個程序就要重新鏈接宇智,發(fā)布給用戶,
動態(tài)鏈接
動態(tài)鏈接的出現(xiàn)解決了上面的問題胰丁。將程序模塊相互獨立的分隔開來随橘,形成獨立的文件,不再將它們靜態(tài)地鏈接到一起锦庸。簡單而言就是對那些組成程序目標文件的鏈接机蔗,等到程序運行時才進行鏈接,也就是把鏈接的過程推遲到運行時才進行,這就是動態(tài)鏈接的基本思想萝嘁。
如上面的例子梆掸,假如現(xiàn)在保留了Program1.o、Program2.o和Lib.o牙言,當運行Program1這個程序的時候沥潭,系統(tǒng)首先加載Program1.o,當系統(tǒng)發(fā)現(xiàn)Program1.o依賴Lib.o的時候嬉挡,那么系統(tǒng)再去加載Lib.o钝鸽,如果還依賴其他目標文件,則同樣以類似于懶加載的方式去加載其他目標文件庞钢。
當所有的目標文件加載完之后拔恰,依賴關(guān)系也得到了滿足,則系統(tǒng)才開始進行鏈接基括,這個鏈接過程和現(xiàn)在鏈接非常相似颜懊。之前介紹過靜態(tài)鏈接的過程,包含符號解析风皿,重定向等河爹。完成這些之后,系統(tǒng)再把控制權(quán)交過Program1.o的執(zhí)行入口桐款,開始執(zhí)行咸这。如果這個時候Program2需要運行,則會發(fā)現(xiàn)系統(tǒng)中已經(jīng)存在了Lib.o的副本所以就不需要重新加載Lib.o魔眨,直接將Lib.o鏈接起來就可以了媳维。
圖示:
根據(jù)前面介紹的,這樣的方式不僅僅減少了內(nèi)存遏暴、磁盤空間的浪費侄刽,還減少了物理頁面的換入換出,也可以增加CPU緩存的命中率朋凉,因為不同進程的數(shù)據(jù)和指令偶讀集中在了一個共享模塊上州丹。
至于更新也就更加簡單了,只需要簡單的將舊的目標文件覆蓋掉杂彭。無需從先將程序鏈接一遍墓毒,下次程序運行的時候,新的目標文件就會自動裝載到內(nèi)存中盖灸。
擴展性及兼容性
動態(tài)鏈接還有一個特點就是程序在運行時可以動態(tài)地選擇加載各種程序模塊蚁鳖,這個優(yōu)點被用來制作插件。
動態(tài)鏈接還可以加強程序的兼容性赁炎。一個程序在不同的平臺運行時可以動態(tài)地鏈接到由操作系統(tǒng)提供的動態(tài)鏈接庫,這些動態(tài)鏈接庫相當于在程序和操作系統(tǒng)之間增加一個中間層,從而消除了程序?qū)Σ煌脚_之間依賴的差異性徙垫。
比如操作系統(tǒng)A和B對于printf方法的實現(xiàn)機制不同讥裤,那么如果是靜態(tài)鏈接,程序需要分別鏈接成能夠運行A和B的兩個版本并且分開發(fā)布姻报,要是動態(tài)鏈接己英,需要A和B能夠體統(tǒng)一個動態(tài)鏈接庫包含printf方法,且這個方法使用一樣的接口吴旋,那么程序只需要一個版本损肛,就可以在兩個OS上跑起來了
動態(tài)鏈接的基本實現(xiàn)
動態(tài)鏈接的基本思想是把程序按照模塊拆分成各個相對獨立部分,在程序運行時才將它們鏈接在一起荣瑟,形成一個完整程序治拿,而不是像靜態(tài)鏈接一樣把所有的程序模塊鏈接成一個單獨的可執(zhí)行文件。
動態(tài)鏈接涉及運行時的鏈接及多個文件的轉(zhuǎn)載笆焰,必需要有操作系統(tǒng)的支持劫谅,因為動態(tài)鏈接的情況下,進程的虛擬地址空間的分布會比靜態(tài)鏈接情況下更為復雜嚷掠,還有一些存儲管理捏检、內(nèi)存共享、進程線程等機制在動態(tài)鏈接下也會有一些微妙變化不皆。
Linux系統(tǒng)中贯城,ELF動態(tài)鏈接文件被稱為動態(tài)共享對象,簡稱共享對象霹娄,它們一般是”.so”文件冤狡。在windows系統(tǒng)中,動態(tài)鏈接被稱為動態(tài)鏈接庫项棠,它們通常就是我們常見的”.dll”為擴展名的文件悲雳。
當程序被轉(zhuǎn)載的時候,系統(tǒng)的動態(tài)鏈接器會將程序所需要的所有動態(tài)鏈接庫裝載到進程的地址空間香追,并將程序中所有未決議的符號綁定到相應(yīng)的動態(tài)鏈接庫中合瓢,并進行重定位工作。
Linux中透典,常用C語言庫的運行庫glibc晴楔,它的動態(tài)鏈接形式版本保存在“/lib”目錄下,文件名叫做libc.so峭咒,整個系統(tǒng)只保留了一份C語言庫的動態(tài)鏈接文件libc.so税弃,而所有C編程的,動態(tài)鏈接的程序都可以在運行時使用它凑队,當程序被裝載時则果,系統(tǒng)的動態(tài)鏈接器會將程序所需要的所有動態(tài)鏈接庫裝載到進程的地址空間,并且將程序中所欲未決議的符號綁定到相應(yīng)的動態(tài)鏈接庫中,并進行重定位工作
簡單動態(tài)鏈接例子
演示代碼文件:
Program1.c
#include "Lib.h"
int main()
{
foobar(1);
return 0;
}
Program2.c
#include "Lib.h"
int main()
{
foobar(2);
return 0;
}
Lib.c
#include <stdio.h>
void foobar(int i) {
printf("Printint from Lib.so %d\n", i);
}
Lib.h
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
使用GCC將Lib.c編譯成一個共享對象文件:
gcc -fPIC -shared -o Lib.so Lib.c
參數(shù):
- -shared表示產(chǎn)生共享對象
可以得到一個Lib.so文件西壮。這就是包含了Lib.c的foobar函數(shù)的共享對象文件
接下來再分別編譯Program1.c & Program2.c
gcc -o Program1 Program1.c ./Lib.so
gcc -o Program2 Program2.c ./Lib.so
從Program1的角度看遗增,整個編譯和鏈接過程圖示:
Lib.c被編譯成Lib.so共享對象文件,Program1.c被編譯成Program1.o之后款青,鏈接稱為可執(zhí)行文件Program1做修,但上圖中有一步與靜態(tài)鏈接不一樣,那就是Program1.o被鏈接成可執(zhí)行文件這一步抡草,在靜態(tài)鏈接中饰及,這一步鏈接過程會把Program1.o & Lib.o鏈接到一起,并且產(chǎn)生可執(zhí)行文件Program1康震,但是這里Lib.o沒有被鏈接進來燎含,鏈接的輸入目標文件只有Program1.o,但是命令行中可以發(fā)現(xiàn)Lib.so參與了鏈接過程
模塊
靜態(tài)鏈接中,整個程序最終只有一個可執(zhí)行文件签杈,它是一個不可以分割的整體瘫镇,但是在動態(tài)鏈接下,一個程序被分成了若干個文件答姥,有程序的主要部分铣除,即可執(zhí)行文件(Program1)和
程序所依賴的共享對象(Lib.so),很多時候把這些部分叫做模塊鹦付,即動態(tài)鏈接下的可執(zhí)行文件和共享對象都可以看做是程序的一個模塊
當程序模塊Program1.c被編譯成Program1.o時尚粘,編譯器還不不知道foobar函數(shù)的地址,當鏈接器將Program1.o鏈接成可執(zhí)行文件時敲长,這時候鏈接器必須確定Program1.o中所引用的foobar函數(shù)的性質(zhì)郎嫁。
如果foobar是一個定義與其它靜態(tài)目標模塊中函數(shù),那么鏈接器將會按照靜態(tài)鏈接的規(guī)則祈噪,將Program1.o中的foobar地址引用重定位
如果foobar是一個定義在某個動態(tài)共享對象中的函數(shù)泽铛,那么鏈接器就會將這個符號的引用標記為一個動態(tài)鏈接的符號,不對它進行地址重定位辑鲤,把這個過程留到裝載時再進行
這就引出了一個問題盔腔?
鏈接器如何知道foobar的引用是一個靜態(tài)符號還是一個動態(tài)符號?實際上這就是用到Lib月褥,so的原因弛随,Lib.so中保存了完整的符號信息,把Lib.so也作為鏈接的輸入文件之一宁赤,鏈接器在解析符號時就知道:foobar是一個定義在Lib.so的動態(tài)符號舀透,這樣鏈接器就可以對foobar的引用做特殊的處理,使它成為一個對動態(tài)符號的引用
動態(tài)鏈接程序運行時地址空間分布
靜態(tài)鏈接而言决左,整個進程只有一個可執(zhí)行文件被映射愕够,之前介紹過靜態(tài)的內(nèi)存分布走贪。動態(tài)鏈接而言除了可執(zhí)行文件外還有其他共享目標文件。
在Lib.c中的foobar加入sleep函數(shù)防止一運行程序就結(jié)束了链烈。
#include<stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n",i);
sleep(-1);
}
然后查看進程的虛擬地址空間分布:
$ ./Program1 &
[1] 4471
$ cat /proc/4471/maps
55fc314d9000-55fc314da000 r-xp 00000000 08:01 1179978 /home/mrlin/桌面/project/p6/Program1
55fc316d9000-55fc316da000 r--p 00000000 08:01 1179978 /home/mrlin/桌面/project/p6/Program1
55fc316da000-55fc316db000 rw-p 00001000 08:01 1179978 /home/mrlin/桌面/project/p6/Program1
55fc31df4000-55fc31e15000 rw-p 00000000 00:00 0 [heap]
7fb5fba9d000-7fb5fbc84000 r-xp 00000000 08:01 1185496 /lib/x86_64-linux-gnu/libc-2.27.so
7fb5fbc84000-7fb5fbe84000 ---p 001e7000 08:01 1185496 /lib/x86_64-linux-gnu/libc-2.27.so
7fb5fbe84000-7fb5fbe88000 r--p 001e7000 08:01 1185496 /lib/x86_64-linux-gnu/libc-2.27.so
7fb5fbe88000-7fb5fbe8a000 rw-p 001eb000 08:01 1185496 /lib/x86_64-linux-gnu/libc-2.27.so
7fb5fbe8a000-7fb5fbe8e000 rw-p 00000000 00:00 0
7fb5fbe8e000-7fb5fbe8f000 r-xp 00000000 08:01 1179974 /home/mrlin/桌面/project/p6/Lib.so
7fb5fbe8f000-7fb5fc08e000 ---p 00001000 08:01 1179974 /home/mrlin/桌面/project/p6/Lib.so
7fb5fc08e000-7fb5fc08f000 r--p 00000000 08:01 1179974 /home/mrlin/桌面/project/p6/Lib.so
7fb5fc08f000-7fb5fc090000 rw-p 00001000 08:01 1179974 /home/mrlin/桌面/project/p6/Lib.so
7fb5fc090000-7fb5fc0b7000 r-xp 00000000 08:01 1185468 /lib/x86_64-linux-gnu/ld-2.27.so
7fb5fc29f000-7fb5fc2a2000 rw-p 00000000 00:00 0
7fb5fc2b5000-7fb5fc2b7000 rw-p 00000000 00:00 0
7fb5fc2b7000-7fb5fc2b8000 r--p 00027000 08:01 1185468 /lib/x86_64-linux-gnu/ld-2.27.so
7fb5fc2b8000-7fb5fc2b9000 rw-p 00028000 08:01 1185468 /lib/x86_64-linux-gnu/ld-2.27.so
7fb5fc2b9000-7fb5fc2ba000 rw-p 00000000 00:00 0
7ffe34baa000-7ffe34bcb000 rw-p 00000000 00:00 0 [stack]
7ffe34bdc000-7ffe34bdf000 r--p 00000000 00:00 0 [vvar]
7ffe34bdf000-7ffe34be1000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
整個進程調(diào)度虛擬地址空間多出了幾個文件的映射厉斟。Lib.so與Program1一樣挚躯,被操作系統(tǒng)以同樣的方式映射到虛擬地址空間强衡,只是占據(jù)的虛擬地址范圍不同。
Program1除了使用Lib.so移位码荔,其中還用到了動態(tài)鏈接形式的C語言運行庫libc-2.27.so漩勤,還有一個非常重要的共享對象ld-2.27.so,其實ld-2.27.so就是Linux下的動態(tài)鏈接器缩搅。動態(tài)鏈接器和普通的共享對象一樣被映射到了進程的地址空間越败,系統(tǒng)開始運行程序之前,會把控制權(quán)給動態(tài)鏈接器硼瓣,由動態(tài)鏈接器完成鏈接工作究飞,之后再把控制權(quán)給Program1
通過readelf工具查看Lib.so的裝載屬性:
readelf -l Lib.so
Elf 文件類型為 DYN (共享目標文件)
Entry point 0x580
There are 7 program headers, starting at offset 64
程序頭:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000754 0x0000000000000754 R E 0x200000
LOAD 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x0000000000000220 0x0000000000000228 RW 0x200000
DYNAMIC 0x0000000000000e20 0x0000000000200e20 0x0000000000200e20
0x00000000000001c0 0x00000000000001c0 RW 0x8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
0x0000000000000024 0x0000000000000024 R 0x4
GNU_EH_FRAME 0x00000000000006b4 0x00000000000006b4 0x00000000000006b4
0x0000000000000024 0x0000000000000024 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
0x00000000000001f0 0x00000000000001f0 R 0x1
Section to Segment mapping:
段節(jié)...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .dynamic .got
可以看到除了文件類型和可執(zhí)行文件不同與裝載地址從0x0000 0000開始之外,其余基本上都一樣堂鲤。很明顯這個裝載地址是無效地址亿傅。共享對象最終的裝載地址在編譯時是不確定的,而是在裝載的時候瘟栖,裝載器根據(jù)當前地址空間的空閑情況葵擎,動態(tài)分配一塊足夠大小的虛擬地址空間給相應(yīng)的共享對象。
地址無關(guān)代碼
固定裝載地址的困擾
問題?
共享對象被裝載時半哟,如何確定它在進程虛擬地址空間中的位置酬滤?
為了實現(xiàn)動態(tài)鏈接,第一個問題就是共享對象地址的沖突問題寓涨,程序模塊的指令和數(shù)據(jù)中可能會包含一些絕對地址的引用盯串,在鏈接產(chǎn)生輸出文件時候,就要假設(shè)模塊被裝載的目標地址
動態(tài)鏈接下戒良,不同的模塊目標裝載地址都一樣是不行的体捏,對于單個程序,可以手工指定各個模塊的地址蔬墩,如把0x1000到0x2000分配給模塊A译打,把多少到多少分配給B,但是一旦模塊多了或者使用的人多起來之后拇颅,就很麻煩奏司。
早期系統(tǒng)就采用這種方法,叫做靜態(tài)共享庫
靜態(tài)共享庫和靜態(tài)庫有很明顯的區(qū)別樟插。靜態(tài)庫是在鏈接的時候就確定了符號地址韵洋,而靜態(tài)共享庫是吧程序各個模塊統(tǒng)一交給操作系統(tǒng)來管理竿刁,操作系統(tǒng)在某個特定的地址劃分出一個地址塊,為已知的模塊預留足夠的空間搪缨。
靜態(tài)共享庫有很多問題食拜,比如地址沖突;還有就是升級之后共享庫必須保持共享庫中的全局函數(shù)和變量地址不變副编,一旦在鏈接的時候綁定了這些地址负甸,更改之后就需要重新鏈接整個程序。
裝載時重定位
為了讓共享對象在任意地址裝載痹届,所以對所有絕對地址的引用不做重定位呻待,而是把這步推遲到裝載的時候再完成,比如一旦模塊的裝載地址確定了也就是目標地址確定队腐,那么系統(tǒng)對程序所有的絕對地址引用進行重定位蚕捉,來實現(xiàn)任意地址裝載。
比如前面的例子foorbar相對于代碼段的其實位置是0x100柴淘,當模塊被裝載到0x10000000時迫淹,假設(shè)代碼段在模塊最開始的位置,則foobar的地址就是0x10000100为严。這個時候遍歷所有模塊中的重定位表敛熬,把所有對foorbar的地址引用都重定位為0x10000100
類似這種方法很早就就存在了,早先沒有虛擬存儲概念下梗脾,程序是直接裝載進入物理內(nèi)存的
比如一個程序在編譯時假設(shè)被裝載的目標地址為0x1000,但是裝載時發(fā)現(xiàn)這個地址被別的程序使用了荸型,所從0x4000開始有足夠大的空間可以容納該程序,那么程序就裝載到0x4000
前面在靜態(tài)鏈接時提到過重定位炸茧,那時的重定位叫做鏈接時重定位(Link Time Relocation)瑞妇,而現(xiàn)在這種情況經(jīng)常被稱為裝載時重定位(Load Time Relocation),在Windows中梭冠,這種裝載時重定位又被叫做基址重置(Rebasing)辕狰。
但是裝載時重定位的方法并不適合用來解決上面的共享對象中所存在的問題】啬可以想象蔓倍,動態(tài)鏈接模塊被裝載映射至虛擬空間后,指令部分是在多個進程之間共享的盐捷,由于裝載時重定位的方法需要修改指令偶翅,所以沒有辦法做到同一份指令被多個進程共享,因為指令被重定位后對于每個進程來講是不同的碉渡。當然聚谁,動態(tài)鏈接庫中的可修改數(shù)據(jù)部分對于不同的進程來說有多個副本,所以它們可以采用裝載時重定位的方法來解決滞诺。Linux和GCC支持這種裝載時重定位的方法形导,我們前面在產(chǎn)生共享對象時环疼,使用了兩個GCC參數(shù)“-shared”和“-FPIC”,如果只使用“-shared”朵耕,那么輸出的共享對象就是使用裝載時重定位的方法炫隶。
地址無關(guān)代碼
什么是"-fPIC"?這個參數(shù)有什么效果?
裝載時重定位是解決動態(tài)模塊中有絕對地址引用的辦法之一,但是它有一個很大的缺點是指令部分無法在多個進程之間共享阎曹,這樣就失去了動態(tài)鏈接節(jié)省內(nèi)存的一大優(yōu)勢伪阶。我們還需要有一種更好的方法解決共享對象指令中對絕對地址的重定位問題。其實我們的目的很簡單芬膝,希望程序模塊中共享的指令部分在裝載時不需要因為裝載地址的改變而改變望门,所以實現(xiàn)的基本想法就是把指令中那些需要被修改的部分分離出來形娇,跟數(shù)據(jù)部分放在一起锰霜,這樣指令部分就可以保持不變,而數(shù)據(jù)部分可以在每個進程中擁有一個副本桐早。這種方案就是目前被稱為地址無關(guān)代碼(PIC, Position-independent Code)的技術(shù)癣缅。
對于現(xiàn)代的機器來說,產(chǎn)生地址無關(guān)的代碼并不麻煩哄酝。我們先來分析模塊中各種類型的地址引用方式:這里把共享對象模塊中的地址引用按照是否為跨模塊分成兩類:模塊內(nèi)部引用和模塊外部引用友存;按照不同的引用方式又可以分為指令引用和數(shù)據(jù)訪問,這樣就得到了四種情況陶衅,如下圖所示:
1 模塊內(nèi)部的函數(shù)調(diào)用 跳轉(zhuǎn)等
2 模塊內(nèi)部的數(shù)據(jù)訪問屡立,比如模塊中定義的全局變量,靜態(tài)變量
3 模塊外部的函數(shù)調(diào)用搀军,跳轉(zhuǎn)等
4 模塊外部的數(shù)據(jù)訪問膨俐,比如其他模塊中定義的全局變量
static int a;
extern int b;
extern void ext();
void bar()
{
a = 1; //對應(yīng)第2
b = 2;//對應(yīng)第4
}
void foo()
{
bar();//對應(yīng)第1
ext();//對應(yīng)第3
}
編譯器編譯pic.c時,它實際上不能確定變量b和函數(shù)ext()是模塊外部的還是模塊內(nèi)部的罩句,因為它們有可能被定義在同一個共享對象的其它目標文件中焚刺,所以編譯器只能把它們都當做模塊外部的函數(shù)和變量來處理
類型一 模塊內(nèi)部的函數(shù)調(diào)用、跳轉(zhuǎn)等
被調(diào)用的函數(shù)與調(diào)用者都處于同一個模塊门烂,它們之間的相對位置是固定的乳愉,所以這種情況比較簡單。對于現(xiàn)代的系統(tǒng)來講屯远,模塊內(nèi)部的跳轉(zhuǎn)蔓姚、函數(shù)調(diào)用都可以是相對地址調(diào)用,或者是基于寄存器的相對調(diào)用慨丐,所以對于這種指令是不需要重定位的坡脐。
類型二 模塊內(nèi)部的數(shù)據(jù)訪問,比如模塊中定義的全局變量咖气、靜態(tài)變量
指令中不能直接包含數(shù)據(jù)的絕對地址挨措,那么唯一的辦法就是相對尋址挖滤。我們知道,一個個模塊前面一般是若干個頁的代碼浅役,后面緊跟著若干個頁的數(shù)據(jù)斩松,這些頁之間的相對位置是固定的,也就是說觉既,任何一條指令與它需要訪問的模塊內(nèi)部數(shù)據(jù)之間的相對位置是固定的惧盹,那么只需要相對于當前指令加上固定的偏移量就可以訪問模塊內(nèi)部數(shù)據(jù)了。現(xiàn)代的體系結(jié)構(gòu)中瞪讼,數(shù)據(jù)的相對尋址往往沒有相對于當前指令地址(PC)的尋址方式钧椰,所以ELF用了一個很巧妙的辦法來得到當前的PC值,然后再加上一個偏移量就可以達到訪問相應(yīng)變量的目的了符欠。得到PC值的方法很多嫡霞。
類型三 模塊外部的數(shù)據(jù)訪問,比如其它模塊中定義的全局變量
模塊間的數(shù)據(jù)訪問目標地址要等到裝載時才決定希柿,比如上面例子中的變量b诊沪,它被定義在其它模塊中,并且該地址在裝載時才能確定曾撤。要使得代碼地址無關(guān)端姚,基本的思想就是把跟地址相關(guān)的部分放到數(shù)據(jù)段里面,很明顯挤悉,這些其它模塊的全局變量的地址是跟模塊裝載地址有關(guān)的渐裸。ELF的做法是在數(shù)據(jù)段里面建立一個指向這些變量的指針數(shù)組,也被稱為全局偏移表(Global Offset Table, GOT)装悲,當代碼需要引用該全局變量時昏鹃,可以通過GOT中相對應(yīng)的項間接引用。當指令中需要訪問變量b時衅斩,程序會先找到GOT盆顾,然后根據(jù)GOT中變量所對應(yīng)的項找到變量的目標地址。每個變量都對應(yīng)一個4個字節(jié)的地址畏梆,鏈接器在裝載模塊的時候會查找每個變量所在的地址您宪,然后填充GOT中的各個項,以確保每個指針所指向的地址正確奠涌。由于GOT本身是放在數(shù)據(jù)段的宪巨,所以它可以在模塊裝載時被修改,并且每個進程都可以有獨立的副本溜畅,相互不受影響捏卓。
GOT如何做到指令的地址無關(guān)性?從第二種類型的數(shù)據(jù)訪問我們了解到慈格,模塊在編譯時可以確定模塊內(nèi)部變量相對于當前指令的偏移怠晴,那么我們也可以在編譯時確定GOT相對于當前指令的偏移遥金。確定GOT的位置跟上面的訪問變量a的方法基本一樣,通過得到PC值然后加上一個偏移量蒜田,就可以得到GOT的位置稿械。然后我們根據(jù)變量地址在GOT中的偏移就可以得到變量的地址,當然GOT中每個地址對應(yīng)于哪個變量是由編譯器決定的冲粤。
類型四 模塊外部的函數(shù)調(diào)用美莫、跳轉(zhuǎn)等
也可以采用上面類型三的方法來解決,與上面的類型有所不同的是梯捕,GOT中相應(yīng)的項保存的是目標函數(shù)的地址厢呵,當模塊需要調(diào)用目標函數(shù)時,可以通過GOT中的項進行間接跳轉(zhuǎn)傀顾。
各種地址引用方式:
| |指令跳轉(zhuǎn) & 調(diào)用 |數(shù)據(jù)訪問 |
|-|-|-|-
|模塊內(nèi)部|1 相對跳轉(zhuǎn)和調(diào)用|2 相對地址訪問|
|模塊外部|3間接跳轉(zhuǎn)&調(diào)用(GOT)|4 間接訪問(GOT)
-fpic和-fPIC
使用GCC產(chǎn)生地址無關(guān)代碼很簡單襟铭,我們只需要使用“-fPIC”參數(shù)接口。實際上GCC還提供了另外一個類似的參數(shù)叫做“-fpic”锣笨,即”PIC”3個字母小寫蝌矛,這兩個參數(shù)從功能上來講完全一樣,都是指示GCC產(chǎn)生地址無關(guān)代碼错英。唯一的區(qū)別是,“-fPIC”產(chǎn)生的代碼要大隆豹,而“-fpic”產(chǎn)生的代碼相對較小椭岩,而且較快。那么我們?yōu)槭裁床皇褂谩?fpic”而要使用“-fPIC”呢璃赡?原因是判哥,由于地址無關(guān)代碼都是跟硬件平臺相關(guān)的,不同的平臺有著不同的實現(xiàn)碉考,“-fpic”在某些平臺上會有一些限制塌计,比如全局符號的數(shù)量或者代碼的長度等,而“-fpic”則沒有這樣的限制侯谁。所以為了方便起見锌仅,絕大部分情況下,我們都使用“-fPIC”參數(shù)來產(chǎn)生地址無關(guān)代碼墙贱。
$ readelf -d Lib.so | grep TEXTREL
上面的命令可以用來區(qū)分一個DSO是否為PIC。如果上面的命令有任何輸出,那么Lib.so就不是PIC的轴合,否則就是PIC的晓殊。PIC的DSO是不會包含任何代碼段重定位表的,TEXTREL表示代碼段重定位表地址魁衙。
PIC與PIE
地址無關(guān)代碼技術(shù)除了可以用在共享對象上面报腔,它也可以用于可執(zhí)行文件株搔,一個以地址無關(guān)方式編譯的可執(zhí)行文件被稱作地址無關(guān)可執(zhí)行文件(PIE, Position-Independent Executable)。與GCC的“-fPIC”與”“-fpic”參數(shù)類似纯蛾,產(chǎn)生PIE的參數(shù)為“-fPIE”或“-fpie”
共享模塊的全局變量問題
定義在模塊內(nèi)的全局變量邪狞?當一個模塊引用了一個定義在全局變量的時候,編譯器無法判斷這個變量在定義同一模塊還是定義在另一個共享對象之中茅撞。
比如:
一個共享對象定義了一個全局變量global帆卓,而模塊module.c中這么引用:
extern int global;
int foo()
{
golbal = 1;
}
當編譯器編譯module.c時,它無法根據(jù)上下文判斷global是定義在同一個模塊的其它目標文件還是定義在另外一個共享對象之中米丘,即無法判斷是否為跨模塊間的調(diào)用
數(shù)據(jù)段地址無關(guān)性
數(shù)據(jù)部分是否也有絕對地址引用問題?
代碼:
static int a;
static int* p = &a;
上面代碼剑令,指針p的地址就是一個絕對地址,它指向變量a拄查,a的地址會隨著共享對象的裝載地址改變而改變
對于數(shù)據(jù)段來說吁津,它在每個進程都有一份獨立的副本,所以并不擔心被進程改變堕扶。從這點來看碍脏,我們可以選擇裝載時重定位的方法來解決數(shù)據(jù)段中絕對地址引用問題。
對于共享對象來說稍算,如果數(shù)據(jù)段中有絕對地址引用典尾,那么編譯器和鏈接器就會產(chǎn)生一個重定位表,這個重定位表里面包含了“R_386_RELATIVE”類型的重定位入口糊探,用于解決上述問題钾埂。當動態(tài)鏈接器裝載共享對象時,如果發(fā)現(xiàn)該共享對象有這樣的重定位入口科平,那么動態(tài)鏈接器就會對該共享對象進行重定位褥紫。
實際上,我們甚至可以讓代碼段也使用這種裝載時重定位的方法瞪慧,而不使用地址無關(guān)代碼髓考。但是,如果代碼不是地址無關(guān)的弃酌,它就不能被多個進程之間共享氨菇,于是也就失去了節(jié)省內(nèi)存的優(yōu)點。但是裝載時重定位的共享對象的運行速度要比使用地址無關(guān)代碼的共享對象快矢腻,因為它省去了地址無關(guān)代碼中每次訪問全局數(shù)據(jù)和函數(shù)時需要做一次計算當前地址以及間接地址尋址的過程门驾。
對于可執(zhí)行文件來說,默認情況下多柑,如果可執(zhí)行文件是動態(tài)鏈接的奶是,那么GCC會使用PIC的方法來產(chǎn)生可執(zhí)行文件的代碼段部分,以便于不同的進程能夠共享代碼段,節(jié)省內(nèi)存聂沙。所以我們可以看到秆麸,動態(tài)鏈接的可執(zhí)行文件中存在”.got”這樣的段。
延遲綁定(PLT)
動態(tài)鏈接的確有很多優(yōu)勢及汉,比靜態(tài)鏈接要靈活得多沮趣,但它是以犧牲一部分性能為代價的。據(jù)統(tǒng)計ELF程序在靜態(tài)鏈接下要比動態(tài)庫稍微快點坷随,當然這取決于程序本身的特性及運行環(huán)境等房铭。
我們知道動態(tài)鏈接比靜態(tài)鏈接慢的主要原因是動態(tài)鏈接下對于全局和靜態(tài)的數(shù)據(jù)訪問都要進行復雜的GOT定位,然后間接尋址温眉;對于模塊間的調(diào)用也要先定位GOT缸匪,然后再進行間接跳轉(zhuǎn),如此一來类溢,程序的運行速度必定會減慢凌蔬。
另外一個減慢運行速度的原因是動態(tài)鏈接的鏈接工作在運行時完成,即程序開始執(zhí)行時闯冷,動態(tài)鏈接器都需要進行一次鏈接工作砂心,動態(tài)鏈接器會尋找并裝載所需要的共享對象,然后進行符號查找地址重定位等工作蛇耀,這些工作勢必減慢程序的啟動速度辩诞。這是影響動態(tài)鏈接性能的兩個主要問題。
延遲綁定實現(xiàn)
在動態(tài)鏈接下蒂窒,程序模塊之間包含了大量的函數(shù)引用(全局變量往往比較少躁倒,因為大量的全局變量會導致模塊之間耦合度變大),所以在程序開始執(zhí)行前洒琢,動態(tài)鏈接會耗費不少時間用于解決模塊之間的函數(shù)引用的符號查找以及重定位。不過可以想象褐桌,在一個程序運行過程中衰抑,可能很多函數(shù)在程序執(zhí)行完時都不會被用到,比如一些錯誤處理函數(shù)或者是一些用戶很少用到的功能模塊等荧嵌,如果一開始就把所有函數(shù)都鏈接好實際上是一種浪費呛踊。
所以ELF采用了一種延遲綁定(Lazy Bingding)的做法,基本的思想
就是當函數(shù)第一次被用到時才進行綁定(符號查找啦撮、重定位等)谭网,如果沒有用到則不進行綁定。所以程序開始執(zhí)行時赃春,模塊間的函數(shù)調(diào)用都沒有進行綁定愉择,而是需要用到時才由動態(tài)鏈接器來負責綁定。這樣的做法可以大大加快程序的啟動速度,特別有利于一些有大量函數(shù)引用和大量模塊的程序锥涕。
ELF使用PLT(Procedure Linkage Table)的方法來實現(xiàn)延遲綁定衷戈,這種方法使用了一些很精巧的指令序列來完成。
假設(shè)liba.so需要調(diào)用libc.so中的bar函數(shù)层坠,那么當liba.so第一次調(diào)用bar時殖妇,這時候就需要調(diào)用動態(tài)鏈接器中的某個函數(shù)來完成地址綁定工作,假設(shè)這個函數(shù)即lookup(),那么lookup()需要知道哪些必要的信息才能完成這個函數(shù)地址綁定工作破花?
- lookup()至少需要知道這個地址綁定發(fā)生在哪個模塊谦趣?哪個函數(shù)?
假設(shè)lookup的原型為lookup(module座每,function)前鹅,這兩個參數(shù)的值在例子中分別為liba.so & bar(),
當我們調(diào)用某個外部模塊的函數(shù)時,如果按照通常的做法應(yīng)該是通過GOT中相應(yīng)的項進行間接跳轉(zhuǎn)尺栖。PLT為了實現(xiàn)延遲綁定嫡纠,在這個過程中間又增加了一層間接跳轉(zhuǎn)。調(diào)用函數(shù)并不直接通過GOT跳轉(zhuǎn)延赌,而是通過一個叫做PLT項的結(jié)構(gòu)來進行跳轉(zhuǎn)除盏。每個外部函數(shù)在PLT中都有一個相應(yīng)的項。
ELF將GOT拆分成了兩個表叫做”.got”和”.got.plt”挫以。其中”.got”用來保存全局變量引用的地址者蠕,”.got.plt”用來保存函數(shù)引用的地址
也就是說,所有對于外部函數(shù)的引用全部被分離出來放到了”.got.plt”中掐松。另外”.got.plt”還有一個特殊的地方是它的前三項是有特殊意義的踱侣,分別含義如下:第一項保存的是”.dynamic”段的地址,這個段描述了本模塊動態(tài)鏈接相關(guān)的信息大磺;第二項保存的是本模塊的ID抡句;第三項保存的是_dl_runtime_resolve()的地址。其中第二項和第三項由動態(tài)鏈接器在裝載共享模塊的時候負責將它們初始化杠愧〈疲”.got.plt”的其余項分別對應(yīng)每個外部函數(shù)的引用。PLT在ELF文件中以獨立的段存放流济,
段名通常叫做”.plt”锐锣,因為它本身是一些地址無關(guān)的代碼,所以可以跟代碼段等一起合并成同一個可讀可執(zhí)行的”Segment”被裝載入內(nèi)存绳瘟。
動態(tài)鏈接相關(guān)結(jié)構(gòu)
動態(tài)鏈接下雕憔,可執(zhí)行文件的裝載與靜態(tài)鏈接基本一樣
操作系統(tǒng)讀取可執(zhí)行文件頭部,檢查文件合法性
從頭部中“Program Header”讀取每個“Segment”的虛擬地址糖声,文件地址斤彼,屬性分瘦,將它們映射到進程虛擬空間的相應(yīng)位置
這些操作跟靜態(tài)鏈接下的裝載基本一致,在靜態(tài)鏈接下畅卓,操作系統(tǒng)接著就可以吧控制權(quán)轉(zhuǎn)交給可執(zhí)行文件的入口地址擅腰,然后程序開始執(zhí)行
但在動態(tài)鏈接下,操作系統(tǒng)還不能在裝載完可執(zhí)行文件之后就把控制權(quán)交給可執(zhí)行文件翁潘,因為可執(zhí)行文件依賴于很多共享對象趁冈,此時,可執(zhí)行里面對于很多外部符號的引用還處于無效地址的狀態(tài)拜马,即還沒有跟相應(yīng)的共享對象中的實際位置鏈接起來渗勘,所以在映射完可執(zhí)行文件之后,操作系統(tǒng)會啟動一個動態(tài)鏈接器
Linux下俩莽,動態(tài)鏈接器ld.so其實是一個共享對象旺坠,操作系統(tǒng)同樣通過映射的方式將它加載到進程的地址空間中,操作系統(tǒng)在加載完動態(tài)鏈接器之后扮超,就將控制權(quán)交給動態(tài)鏈接器的入口地址取刃,當動態(tài)鏈接器得到控制權(quán)之后,它開始執(zhí)行一系列自身的初始化操作出刷,然后根據(jù)當前的環(huán)境參數(shù)璧疗,開始對可執(zhí)行文件進行動態(tài)鏈接工作,當所有動態(tài)鏈接工作完成之后馁龟,動態(tài)鏈接器會將控制權(quán)轉(zhuǎn)交給可執(zhí)行文件的入口地址崩侠,程序正式執(zhí)行
".interp"段
系統(tǒng)中哪個才是動態(tài)鏈接器?位置由誰決定坷檩?
實際上却音,動態(tài)鏈接器的位置既不是由系統(tǒng)配置指定,也不是由環(huán)境參數(shù)決定矢炼,而是由ELF可執(zhí)行文件決定系瓢,在動態(tài)鏈接的ELF可執(zhí)行文件中,有一個專門的段叫做“.interp”
可以使用odjdump工具查看:
$ objdump -s a.out
a.out: 文件格式 elf64-x86-64
Contents of section .interp:
0238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0248 7838362d 36342e73 6f2e3200 x86-64.so.2.
內(nèi)容就是一個字符串句灌,這個字符串是可執(zhí)行文件所需要的動態(tài)鏈接器的路徑八拱,在Linux下,可執(zhí)行文件所需要的動態(tài)鏈接器的路徑幾乎都是"/lib/ld-linux.so.2"
Linux中涯塔,操作系統(tǒng)在對可執(zhí)行文件的進行加載時候,它會去尋找裝載該可執(zhí)行文件所需要相應(yīng)的動態(tài)鏈接器清蚀,即“.interp”段指定的路徑的共享對象
Linux可以通過命令行查看一個可執(zhí)行文件所需要的動態(tài)鏈接器的路徑
$ readelf -l a.out | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
“.dynamic”段
動態(tài)鏈接ELF中最重要的結(jié)構(gòu)應(yīng)該是“.dynamic”段匕荸,這個段里面保存了動態(tài)鏈接器所需要的基本信息,比如依賴于哪些共享對象枷邪,動態(tài)鏈接符號表的位置榛搔,動態(tài)鏈接重定位表的位置诺凡,共享對象初始化代碼的地址等等,
使用readelf工具查看:
$ readelf -d Lib.so
Dynamic section at offset 0xe20 contains 24 entries:
標記 類型 名稱/值
0x0000000000000001 (NEEDED) 共享庫:[libc.so.6]
0x000000000000000c (INIT) 0x520
0x000000000000000d (FINI) 0x690
0x0000000000000019 (INIT_ARRAY) 0x200e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0x368
0x0000000000000006 (SYMTAB) 0x230
0x000000000000000a (STRSZ) 163 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4f0
0x0000000000000007 (RELA) 0x448
0x0000000000000008 (RELASZ) 168 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x428
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x40c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
此外践惑,Linux還提供了一個命令用來查看一個程序主模塊或者一個共享庫依賴于哪些共享庫
$ ldd Program1
linux-vdso.so.1 (0x00007ffcaabe0000)
./Lib.so (0x00007fe1e974c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe1e935b000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe1e9b50000)
動態(tài)符號表
完成動態(tài)鏈接腹泌,最關(guān)鍵的還是所依賴的符號和相關(guān)文件的信息
靜態(tài)鏈接中,有一個專門的段叫做符號表“.symtab”,里面保存了所有關(guān)于該目標文件的符號的定義以及引用尔觉,動態(tài)鏈接的符號表實際上與靜態(tài)鏈接類似
如前面例子Program1程序依賴于Lib.so凉袱,引用到了里面的foobar函數(shù),那么對于Program1來說侦铜,往往叫Program1導入了foobar函數(shù)专甩,foobar函數(shù)是Program1的導入函數(shù)
從Lib.so角度,它實際上定義了foobar函數(shù)钉稍,并且提供給其他模塊使用涤躲,叫Lib.so導出了foobar函數(shù),foobar是Lib.so的導出函數(shù)
為了表示動態(tài)鏈接這些模塊之間的符號導入導出關(guān)系贡未,ELF專門有一個叫做動態(tài)符號表的段用來保存這些信息种樱,這個段叫做“.dynsym”
“.dynsym”只保存了與動態(tài)鏈接有關(guān)的符號,對于那些模塊內(nèi)部的符號俊卤,比如模塊私有變量則不保存
readelf工具來查看
$ readelf -sD Lib.so
Symbol table of `.gnu.hash' for image:
Num Buc: Value Size Type Bind Vis Ndx Name
7 0: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 22 _edata
8 0: 0000000000201038 0 NOTYPE GLOBAL DEFAULT 23 _end
9 1: 0000000000201030 0 NOTYPE GLOBAL DEFAULT 23 __bss_start
10 1: 0000000000000520 0 FUNC GLOBAL DEFAULT 9 _init
11 2: 0000000000000690 0 FUNC GLOBAL DEFAULT 13 _fini
12 2: 000000000000065a 51 FUNC GLOBAL DEFAULT 12 foobar
動態(tài)鏈接重定位表
動態(tài)鏈接的可執(zhí)行文件使用PIC方法嫩挤,雖然其代碼段不需要重定位(因為地址無關(guān)),但是數(shù)據(jù)端還是包含了絕對地址的引用瘾蛋,因為代碼段中絕對地址相關(guān)部分被分離了出來俐镐,編程了GOT(全局偏移表),而GOT實際上是數(shù)據(jù)端的一部分哺哼,除了GOT佩抹,數(shù)據(jù)端還可以能包含絕對地址引用。
重定位相關(guān)數(shù)據(jù)結(jié)構(gòu)
和靜態(tài)鏈接類似取董,動態(tài)鏈接重定位表分為.rel.dyn和.rel.plt他們分別相當于.rel.text和.rel.data棍苹。.rel.dyn是對數(shù)據(jù)的修真,位于.got段茵汰,.rel.plt是對函數(shù)的修正位于.got.plt段枢里。
readelf查看一個動態(tài)鏈接的文件的重定位表
$ readelf -r Lib.so
重定位節(jié) '.rela.dyn' at offset 0x448 contains 7 entries:
偏移量 信息 類型 符號值 符號名稱 + 加數(shù)
000000200e10 000000000008 R_X86_64_RELATIVE 650
000000200e18 000000000008 R_X86_64_RELATIVE 610
000000201028 000000000008 R_X86_64_RELATIVE 201028
000000200fe0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fe8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200ff0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
重定位節(jié) '.rela.plt' at offset 0x4f0 contains 2 entries:
偏移量 信息 類型 符號值 符號名稱 + 加數(shù)
000000201018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000201020 000500000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
動態(tài)鏈接時進程堆棧信息初始化
從動態(tài)鏈接器角度看,當操作系統(tǒng)把控制權(quán)交給它的時候蹂午,它將開始做鏈接的工作栏豺,那么它需要知道關(guān)于可執(zhí)行文件和本進程的一些信息,比如可執(zhí)行文件有幾個段豆胸,每個段的屬性奥洼,程序的入口地址等等。這些信息往往由操作系統(tǒng)傳遞給動態(tài)鏈接器晚胡,保存在進程的堆棧里面
可以寫一個程序把堆棧中的初始化信息全部打印出來:
#include <stdio.h>
#include <elf.h>
int main(int argc, char* argv[])
{
void** p = (void**)argv;
printf("%p\n", p);
printf("Argument count: %d\n", *((int*)p - 1));
int i;
for (i = 0; i < argc; ++i)
{
printf("Argument %d: %s\n", i, (char*)*p);
p++;
}
// skip 0
p++;
printf("Environment:\n");
while (*p) {
printf("%s\n", (char*)*p);
p++;
}
// skip 0
p++;
printf("Auxiliary Vectors:\n");
Elf64_auxv_t* aux = (Elf64_auxv_t*)p;
while (aux->a_type != AT_NULL) {
printf("Type: %02ld Value: %#lx\n", aux->a_type, aux->a_un.a_val);
aux++;
}
return 0;
}
輸出灵奖;
0x7ffe605a00c8
Argument count: 0
Argument 0: ./pr
Environment:
CLUTTER_IM_MODULE=xim
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
LESSCLOSE=/usr/bin/lesspipe %s %s
XDG_MENU_PREFIX=gnome-
LANG=zh_CN.UTF-8
MANAGERPID=1882
DISPLAY=:0
INVOCATION_ID=acc39dd9b8c647aeb29dd7db54b97631
GNOME_SHELL_SESSION_MODE=ubuntu
COLORTERM=truecolor
USERNAME=mrlin
XDG_VTNR=2
SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
XDG_SESSION_ID=2
USER=mrlin
DESKTOP_SESSION=ubuntu
QT4_IM_MODULE=xim
TEXTDOMAINDIR=/usr/share/locale/
GNOME_TERMINAL_SCREEN=/org/gnome/Terminal/screen/d4ed0b15_155b_4b4f_89d0_07c180b08c8c
PWD=/home/mrlin/桌面/project/p6
HOME=/home/mrlin
JOURNAL_STREAM=9:36807
TEXTDOMAIN=im-config
SSH_AGENT_PID=2004
QT_ACCESSIBILITY=1
XDG_SESSION_TYPE=x11
XDG_DATA_DIRS=/usr/share/ubuntu:/usr/local/share:/usr/share:/var/lib/snapd/desktop
XDG_SESSION_DESKTOP=ubuntu
DBUS_STARTER_ADDRESS=unix:path=/run/user/1000/bus,guid=1acbc5cacc124a89e1f576115e888be9
GTK_MODULES=gail:atk-bridge
WINDOWPATH=2
TERM=xterm-256color
SHELL=/bin/bash
VTE_VERSION=5202
QT_IM_MODULE=xim
XMODIFIERS=@im=ibus
IM_CONFIG_PHASE=2
DBUS_STARTER_BUS_TYPE=session
XDG_CURRENT_DESKTOP=ubuntu:GNOME
GPG_AGENT_INFO=/run/user/1000/gnupg/S.gpg-agent:0:1
GNOME_TERMINAL_SERVICE=:1.86
XDG_SEAT=seat0
SHLVL=1
LANGUAGE=zh_CN:zh
GDMSESSION=ubuntu
GNOME_DESKTOP_SESSION_ID=this-is-deprecated
LOGNAME=mrlin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus,guid=1acbc5cacc124a89e1f576115e888be9
XDG_RUNTIME_DIR=/run/user/1000
XAUTHORITY=/run/user/1000/gdm/Xauthority
XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
SESSION_MANAGER=local/mrlin-virtual-machine:@/tmp/.ICE-unix/1911,unix/mrlin-virtual-machine:/tmp/.ICE-unix/1911
LESSOPEN=| /usr/bin/lesspipe %s
GTK_IM_MODULE=ibus
_=./pr
Auxiliary Vectors:
Type: 33 Value: 0x7ffe605b7000
Type: 16 Value: 0xf8bfbff
Type: 06 Value: 0x1000
Type: 17 Value: 0x64
Type: 03 Value: 0x55cfc4890040
Type: 04 Value: 0x38
Type: 05 Value: 0x9
Type: 07 Value: 0x7f7c4b6ea000
Type: 08 Value: 0
Type: 09 Value: 0x55cfc4890580
Type: 11 Value: 0x3e8
Type: 12 Value: 0x3e8
Type: 13 Value: 0x3e8
Type: 14 Value: 0x3e8
Type: 23 Value: 0
Type: 25 Value: 0x7ffe605a03e9
Type: 26 Value: 0
Type: 31 Value: 0x7ffe605a2ff3
Type: 15 Value: 0x7ffe605a03f9
動態(tài)鏈接的步驟 & 實現(xiàn)
動態(tài)鏈接器的自舉
我們知道動態(tài)鏈接器本身也是一個共享對象嚼沿,但是事實上它有一些特殊性。對于普通共享對象文件來說瓷患,它的重定位工作由動態(tài)鏈接器來完成骡尽。他也可以依賴其他共享對象,其中的被依賴共享對象由動態(tài)鏈接器負責鏈接和裝載擅编∨氏福可是對于動態(tài)鏈接器來說,它的重定位工作由誰來完成沙咏?它是否可以依賴于其他共享對象辨图?
這是一個“雞生蛋,蛋生雞”的問題肢藐,為了解決這種無休止的循環(huán)故河,動態(tài)鏈接器這個“雞” 必須有些特殊性。
首先是吆豹,動態(tài)鏈接器本身不可以依賴于其他任何共享對象鱼的;
其次是動態(tài)鏈接器本身所需要的全局和靜態(tài)變量和重定位工作由它本身完成。
對于第一個條件我們可以人為的控制痘煤。在編寫動態(tài)鏈接器時必須保證不使用任何系統(tǒng)庫凑阶,運行庫;
對于第二個條件衷快,動態(tài)鏈接器必須在啟動時有一段非常精巧的代碼可以完成這項艱巨的工作而同時又不能使用全局和靜態(tài)變量宙橱。這種具有一定限制條件的啟動代碼往往被稱為自舉(Bootstrap)。
動態(tài)鏈接器入口地址即是自舉代碼的入口蘸拔,當操作系統(tǒng)將進程控制權(quán)交給動態(tài)鏈接器時师郑,動態(tài)鏈接器的自舉代碼即開始運行。自舉代碼首先會找到它自己的GOT调窍。而GOT的第一個入口保存的是“.dynamic”段的偏移地址宝冕,由此找到了動態(tài)連機器本身的“.dynamic”段。通過“.dynamic”的信息邓萨,自舉代碼便可以獲得動態(tài)鏈接器本身的重定位表和符號表等地梨,從而得到動態(tài)鏈接器本身的重定位入口,先將它們?nèi)恐囟ㄎ坏蘅摇倪@一步開始宝剖,動態(tài)鏈接器代碼中才可以使用自己的全局變量和靜態(tài)變量。
實際上在動態(tài)鏈接器的自舉代碼中,除了不可以使用全局變量和靜態(tài)變量之外,甚至不能調(diào)用函數(shù),即動態(tài)鏈接器本身的函數(shù)也不能調(diào)用歉甚。
這是為什么呢?
其實我們在前面分析地址無關(guān)代碼時已經(jīng)提到過,實際上使用PIC模式編譯的共享對象,對于模塊內(nèi)部的函數(shù)調(diào)用也是采用跟模塊外部函數(shù)調(diào)用一樣的方式,即使用 GOT/PLT的方式,所以在 GOT/PLT沒有被重定位之前,自舉代碼不可以使用任何全局變量,也不可以調(diào)用函數(shù)诈闺。
裝載共享對象
完成基本自舉以后,動態(tài)鏈接器將可執(zhí)行文件和鏈接器本身的符號表都合并到一個符號表當中,我們可以稱它為全局符號表( Global Symbol Table)。
然后鏈接器開始尋找可執(zhí)文件所依賴的共享對象,我們前面提到過“.dynamic”段中,有一種類型的入口DT_NEEDED,它所指出的是該可執(zhí)行文件(或共享對象)所依賴的共享對象铃芦。由此,鏈接器可以列出可執(zhí)行文件所需要的所有共享對象,并將這些共享對象的名字放入到一個裝載集合中雅镊。然后鏈接器開始從集合里取個所需要的共享對象的名字,找到相應(yīng)的文件后打開該文件,讀取相應(yīng)的ELF文件頭和“ .dynamic”段,然后將它相應(yīng)的代碼段和數(shù)據(jù)段映射到進程空間中。
如果這個ELF共享對象還依賴于其他共享對象,那么將所依賴的共享對象的名字放到裝載集合中刃滓。如此循環(huán)直到所有依賴的共享對象都被裝載進來為止,當然鏈接器可以有不同的裝載順序,如果我們把依賴關(guān)系看作一個圖的話,那么這個裝載過程就是一個圖的遍歷過程,鏈接器可能會使用深度優(yōu)先或者廣度優(yōu)先或者其他的順序來遍歷整個圖,這取決于鏈接器,比較常見的算法一般都是廣度優(yōu)先的仁烹。
符號優(yōu)先級
a1.c
#include <stdio.h>
void a() {
printf("a1.c\n");
}
a2.c
#include <stdio.h>
void a() {
printf("a2.c\n");
}
b1.c
#include <stdio.h>
void a();
void b1() {
a();
}
b2.c
#include <stdio.h>
void a();
void b2() {
a();
}
a1.c & a2.c都定義了名字為a的函數(shù),那么b1.c & b2.c 都使用了外部函數(shù)a咧虎,但是源代碼中沒有指定依賴于哪個共享對象中的函數(shù)a卓缰,所以在編譯時指定依賴關(guān)系,
假設(shè)b1.so依賴于a1.so,b2.so依賴于a2.so,將b1.so與a1.so進行鏈接砰诵,b2.so與a2.so進行鏈接
$ gcc -fPIC -shared a1.c -o a1.so
$ gcc -fPIC -shared a2.c -o a2.so
$ gcc -fPIC -shared b1.c a1.so -o b1.so
$ gcc -fPIC -shared b2.c a2.so -o b2.so
$ ldd b1.so
linux-vdso.so.1 (0x00007ffc06ff4000)
a1.so => not found
$ ldd b2.so
linux-vdso.so.1 (0x00007ffd56bc7000)
a2.so => not found
當有程序同時使用b1.c的函數(shù)b1和b2.c中的函數(shù)b2是會怎樣征唬?
#include<stdio.h>
void b1();
void b2();
int main()
{
b1();
b2();
return 0;
}
然后我們將main.c編譯成可執(zhí)行文件并且運行:
$ gcc main.c b1.so b2.so -o main -Xlinker -rpath ./
關(guān)于全局符號介入這個問題,實際上Linux下的動態(tài)鏈接器是這樣處理的:
它定義了一個規(guī)則,那就是當一個符號需要被加入全局符號表時,如果相同的符號名已經(jīng)存在,則后加入的符號被忽略從動態(tài)鏈接器的裝載順序可以看到,它是按照廣度優(yōu)先的順序進行裝載的,首先是main,然后是b1.so、b2.so茁彭、a1.so,最后是a2.so总寒。當a2.so中的函數(shù)a要被加入全局符號表時,先前裝載a1.so時,al.o中的函數(shù)a已經(jīng)存在于全局符號表,那么a2.so中的函數(shù)a只能被忽略。所以整個進程中,所有對于符合“a”的引用都會被解析到a1.so中的函數(shù)a,這也是為什么main打印出的結(jié)果是兩個“a1.c”而不是理想中的“alc”和“a2.c”理肺。
由于存在這種重名符號被直接忽略的問題,當程序使用大量共享對象時應(yīng)該非常小心符號的重名問題,如果兩個符號重名又執(zhí)行不同的功能,那么程序運行時可能會將所有該符號名的引用解析到第-個被加入全局符號表的使用該符號名的符號,從而導致程序莫名其妙的錯誤摄闸。
全局符號介入與地址無關(guān)代碼
地址無關(guān)代碼,對于第一類模塊內(nèi)部調(diào)用或跳轉(zhuǎn)的處理時,我們簡單地將其當作是相對地址調(diào)用/跳轉(zhuǎn)。但實際上這個問題比想象中要復雜,結(jié)合全局符號介入,關(guān)于調(diào)用方式的分類的解釋會更加清楚妹萨。還是拿前面“pic.c”的例子來看,由于可能存在全局符號介入的問題,foo函數(shù)對于bar的調(diào)用不能夠采用第一類模塊內(nèi)部調(diào)用的方法,因為一旦bar函數(shù)由于全局符號介入被其他模塊中的同名函數(shù)覆蓋,那么foo如果采用相對地址調(diào)用的話,那個相對地址部分就需要重定位,這又與共享對象的地址無關(guān)性矛盾年枕。所以對于bar()函數(shù)的調(diào)用,編譯器只能采用第三種,即當作模塊外部符號處理,bar()函數(shù)被覆蓋,動態(tài)鏈接器只需要重定位“.got .plt”,不影響共享對象的代碼段
為了提高模塊內(nèi)部函數(shù)調(diào)用的效率,有一個辦法是把bar()函數(shù)變成編譯單元私有函數(shù),即使用“ statIc”關(guān)鍵字定義bar()函數(shù),這種情況下,編譯器要確定bar()函數(shù)不被其他模塊覆蓋,就可以使用第一類的方法,即模塊內(nèi)部調(diào)用指令,可以加快函數(shù)的調(diào)用速度。
重定位與初始化
當上面的步驟完成之后乎完,鏈接器開始重新遍歷可執(zhí)行的文件和每個共享對象的重定位表熏兄,將它們的GOT/PLT的每個需要重定位的位置進行修正。
因為此時動態(tài)鏈接器已經(jīng)擁有了進程的全局符號表树姨,所以這個修正過程也顯得比較容易摩桶,跟我們前面提到的地址重定位的原理基本相同。在前面介紹動態(tài)鏈接的重定位表時娃弓,我們已經(jīng)碰到了幾種重定位類型典格,每種重定位入口地址的計算方法我們在這里就不再重復介紹了。
重定位完成之后台丛,如果某個共享對象有“.init”段耍缴,那么動態(tài)鏈接器會執(zhí)行“.init”段中的代碼,用以實現(xiàn)共享對象特有的初始化過程,比如最常見的,共享對象中的C++ 的全局靜態(tài)對象的構(gòu)造就需要通過“init”來初始化。相應(yīng)地,共享對象中還可能有“ finit”段,當進程退出時會執(zhí)行“.finit"段中的代碼,可以用來實現(xiàn)類似C++全局對象析構(gòu)之類的操作挽霉。
如果進程的可執(zhí)行文件也有“init”段,那么動態(tài)鏈接器不會執(zhí)行它,因為可執(zhí)行文件中的“init”段和“ finit”段由程序初始化部分代碼負責執(zhí)行,我們將在后面的“庫”這部分詳細介紹程序初始化部分防嗡。
當完成了重定位和初始化之后,所有的準備工作就宣告完成了,所需要的共享對象都已經(jīng)裝載并且鏈接完成了,這時候動態(tài)鏈接器就如釋重負,將進程的控制權(quán)轉(zhuǎn)交給程序的入口并且開始執(zhí)行。
Linux動態(tài)鏈接器的實現(xiàn)
在前面分析 Linux下程序的裝載時,己經(jīng)介紹了一個通過 execve()系統(tǒng)調(diào)用被裝載到進程的地址空間的程序,以及內(nèi)核如何處理可執(zhí)行文件侠坎。
內(nèi)核在裝載完ELF可執(zhí)行文件以后就返回到用戶空間,將控制權(quán)交給程序的入口蚁趁。對于不同鏈接形式的ELF可執(zhí)行文件,這個程序的入口是有區(qū)別的
對于靜態(tài)鏈接的可執(zhí)行文件來說,程序的入口就是ELF文件頭里面的 e_entry指定的入口;
對于動態(tài)鏈接的可執(zhí)行文件來說,如果這時候把控制權(quán)交給e_entry指定的入口地址,那么肯定是不行的,因為可執(zhí)行文件所依賴的共享庫還沒有被裝載,也沒有進行動態(tài)鏈接。所以對于動態(tài)鏈接的可執(zhí)行文件,內(nèi)核會分析它的動態(tài)鏈接器地址(在“.interp”段),將動態(tài)鏈接器映射至進程地址空間,然后把控制權(quán)交給動態(tài)鏈接器实胸。
Linux動態(tài)鏈接器是個很有意思的東西,它本身是一個共享對象,它的路徑是lib/ld-linux.so.2,這實際上是個軟鏈接,它指向lib/ld-x.y.z.so,這個才是真正的動態(tài)連接器文件他嫡。共享對象其實也是ELF文件,它也有跟可執(zhí)行文件一樣的EF文件頭(包括 e_entry番官、段表等)。動態(tài)鏈接器是個非常特殊的共享對象,它不僅是個共享對象,還是個可執(zhí)行的程序,可以直接在命令行下面運行:
其實 Linux的內(nèi)核在執(zhí)行 execve()時不關(guān)心目標ELF文件是否可執(zhí)行(文件頭 e_type是 ET_EXEC還是 ET_DYN),它只是簡單按照程序頭表里面的描述對文件進行裝載然后把控制權(quán)轉(zhuǎn)交給ELF入口地址(沒有“.interp”就是ELF文件的 e_entry;如果有“.interp”的話就是動態(tài)鏈接器的 e_entry)钢属。
這樣我們就很好理解為什么動態(tài)鏈接器本身可以作為可執(zhí)行程序運行,這也從一個側(cè)面證明了共享庫和可執(zhí)行文件實際上沒什么區(qū)別,除了文件頭的標志位和擴展名有所不同之外,其他都是一樣的徘熔。 Windows系統(tǒng)中的EXE和DLL也是類似的區(qū)別,DLL也可以被當作程序來運行, Windows提供了一個叫做rund32exe的工具可以把一個DLL當作可執(zhí)行文件運行。