裝載與動(dòng)態(tài)鏈接
可執(zhí)行文件的裝載與進(jìn)程
- 每個(gè)程序都擁有自己獨(dú)立的虛擬地址空間臀稚,這個(gè)空間大小由計(jì)算機(jī)硬件平臺(tái)決定(理論上的最大上限)挡闰。比如,32位硬件平臺(tái)的虛擬地址空間的地址為0到232-1,即0x000000000xFFFFFFFF,總共大概4G;而64位硬件平臺(tái)的虛擬地址空間地址為0到2<sup>64</sup>-1秩伞,即0x00000000000000000xFFFFFFFFFFFFFFFF,大概有17179869184G。在32位平臺(tái)上欺矫,Linux操作系統(tǒng)中4G的虛擬地址空間會(huì)被劃分為兩個(gè)部分纱新,從0xC0000000到0xFFFFFFFF共1G的地址空間被分配給了操作系統(tǒng),剩下的從0x00000000到0xBFFFFFFF共3G的地址空間是留給進(jìn)程的穆趴。從原則上講脸爱,我們進(jìn)程最多能使用3G的虛擬地址空間。對(duì)于Windows操作系統(tǒng)來說未妹,它的進(jìn)程虛擬地址空間劃分是操作系統(tǒng)占用2G簿废,進(jìn)程只剩下2G。對(duì)于一些程序來說2G虛擬空間太小络它,所以Windows有個(gè)啟動(dòng)參數(shù)可以將操作系統(tǒng)占用的虛擬地址空間減少到1G族檬。方法如下:修改Windows系統(tǒng)盤根目錄下的boot.ini,加上“/3G”參數(shù)酪耕。
動(dòng)態(tài)裝載的兩種典型方法是覆蓋裝入和頁映射导梆,覆蓋裝入在沒有發(fā)明虛擬存儲(chǔ)之前使用比較廣泛,現(xiàn)在已經(jīng)幾乎被淘汰了。頁映射簡(jiǎn)單的說就是操作系統(tǒng)將程序需要使用的頁按一定的算法動(dòng)態(tài)映射到物理內(nèi)存中執(zhí)行看尼。
-
從操作系統(tǒng)的角度來看递鹉,一個(gè)進(jìn)程最關(guān)鍵的特征是它擁有獨(dú)立的虛擬地址空間,這使得它有別于其他進(jìn)程藏斩。一個(gè)進(jìn)程的建立有三步:
- 首先是創(chuàng)建虛擬地址空間躏结。
- 讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系狰域。(可執(zhí)行文件裝載中最重要的一步媳拴,也是傳統(tǒng)意義上的“裝載”)
- 將CPU指令寄存器設(shè)置成可執(zhí)行文件入口,啟動(dòng)運(yùn)行兆览。
我們知道屈溉,當(dāng)程序執(zhí)行發(fā)生頁錯(cuò)誤時(shí),操作系統(tǒng)將從物理內(nèi)存中分配一個(gè)物理頁抬探,然后將該“缺頁”從磁盤中讀取到內(nèi)存中子巾,再設(shè)置缺頁的虛擬頁和物理頁的映射關(guān)系,這樣程序才得以正常運(yùn)行小压。但是很明顯的一點(diǎn)是线梗,當(dāng)操作系統(tǒng)捕獲到缺頁錯(cuò)誤時(shí),它應(yīng)知道程序當(dāng)前所需要的頁在可執(zhí)行文件中的哪一個(gè)位置怠益。這就是虛擬空間與可執(zhí)行文件之間的映射關(guān)系仪搔。
ELF文件被映射時(shí),是以系統(tǒng)的頁長(zhǎng)度作為單位的蜻牢。為避免內(nèi)存浪費(fèi)烤咧,操作系統(tǒng)在裝載可執(zhí)行文件時(shí)主要關(guān)心的只是文件中段的權(quán)限(可讀、可寫孩饼、可執(zhí)行)髓削。對(duì)于相同權(quán)限的段,把它們合并到一起當(dāng)作一個(gè)段進(jìn)行映射镀娶。Linux中將進(jìn)程虛擬空間中的一個(gè)段叫做虛擬內(nèi)存區(qū)域(VMA)立膛,在Windows中將這個(gè)叫做虛擬段(Virtual Section)。很多情況下梯码,一個(gè)進(jìn)程中的堆和棧分別都有一個(gè)對(duì)應(yīng)的VMA宝泵。操作系統(tǒng)在進(jìn)程啟動(dòng)前會(huì)將系統(tǒng)的環(huán)境變量和進(jìn)程的運(yùn)行參數(shù)提前保存到進(jìn)程的虛擬空間的棧中(也就是VMA中的stack VMA)。
PE文件的裝載和ELF有所不同轩娶,在PE文件中儿奶,所有段的起始地址都是頁的倍數(shù),段的長(zhǎng)度如果不是頁的整數(shù)倍鳄抒,那么在映射時(shí)向上補(bǔ)齊到頁的整數(shù)倍闯捎。由于這個(gè)特點(diǎn)椰弊,PE文件的映射過程比ELF簡(jiǎn)單得多,因?yàn)樗鼰o需考慮如ELF里面諸多段地址對(duì)齊之類的問題瓤鼻,雖然這樣會(huì)浪費(fèi)一些磁盤和內(nèi)存空間秉版。
PE文件中,鏈接器在生產(chǎn)可執(zhí)行文件時(shí)茬祷,往往將所有的段盡可能地合并清焕,所以一般只有代碼段、數(shù)據(jù)段祭犯、只讀數(shù)據(jù)段和BSS等為數(shù)不多的幾個(gè)段秸妥。
每個(gè)PE文件在裝載時(shí)都會(huì)有一個(gè)裝載目標(biāo)地址,這個(gè)地址就是基地址沃粗,基地址不是固定的粥惧,每次裝載時(shí)都可能會(huì)變化。所以PE文件中有一個(gè)常見術(shù)語叫相對(duì)虛擬地址(RVA)陪每,它是相對(duì)于PE文件的裝載基地址的一個(gè)偏移地址影晓。這樣無論基地址怎么變化,PE文件中的各個(gè)RVA都保持一致檩禾。
-
WIndows PE文件的裝載過程:
- 先讀取文件的第一個(gè)頁(包含DOS頭,PE文件頭和段表)疤祭。
- 檢查進(jìn)程地址空間中盼产,目標(biāo)地址是否可用,如果不可用勺馆,則另外選一個(gè)裝載地址戏售。(主要針對(duì)DLL裝載)
- 使用段表中提供的信息,將PE文件中所有的段一一映射到地址空間中相應(yīng)的位置草穆。
- 如果裝載地址不是目標(biāo)地址灌灾,則進(jìn)行Rebasing。
- 裝載所有PE文件所需要的DLL文件悲柱。
- 對(duì)PE文件中的所有導(dǎo)入符號(hào)進(jìn)行解析锋喜。
- 根據(jù)PE頭中指定的參數(shù),建立初始化堆和棧豌鸡。
- 建立主線程并且啟動(dòng)進(jìn)程嘿般。
動(dòng)態(tài)鏈接
-
為什么要?jiǎng)討B(tài)鏈接?
- 靜態(tài)鏈接的方式對(duì)于計(jì)算機(jī)內(nèi)存和磁盤的空間浪費(fèi)非常嚴(yán)重涯冠。
- 靜態(tài)鏈接對(duì)于程序的更新炉奴、部署和發(fā)布也會(huì)帶來很多麻煩。
在Linux系統(tǒng)中蛇更,ELF動(dòng)態(tài)鏈接文件被成為動(dòng)態(tài)共享對(duì)象(DSO)瞻赶,簡(jiǎn)稱共享對(duì)象赛糟,它們一般都是以“.so”為擴(kuò)展名的一些文件;而在Windows系統(tǒng)中砸逊,動(dòng)態(tài)鏈接文件被成為動(dòng)態(tài)鏈接庫(DLL)璧南,它們通常是以“.dll”為擴(kuò)展名的文件。
靜態(tài)鏈接的重定位叫鏈接時(shí)重定位(Link Time Relocation)痹兜,而動(dòng)態(tài)鏈接的重定位為裝載時(shí)重定位(Load Time Relocation)穆咐,在Windows中,這種裝載時(shí)重定位又被叫做基址重置(Rebasing)字旭。在Linux和GCC中只要使用“-shared”參數(shù)对湃,輸出的共享對(duì)象就是使用的裝載時(shí)重定位。
把指令中那些需要修改的部分分離出來遗淳,跟數(shù)據(jù)部分放在一起拍柒,這樣指令部分就可以保持不變,而數(shù)據(jù)部分可以在每個(gè)進(jìn)程中擁有一個(gè)副本屈暗,這種方案就是地址無關(guān)代碼(PIC)技術(shù)拆讯。在Linux共享對(duì)象中要生成地址無關(guān)代碼只用在編譯是帶上參數(shù)-fPIC。
上面的情況并沒有包括定義在共享模塊內(nèi)部的全局變量养叛。ELF共享庫在編譯時(shí)种呐,默認(rèn)都把定義在模塊內(nèi)部的全局變量當(dāng)做定義在其他模塊的全局變量,也就是說當(dāng)做上圖中的類型(4)弃甥,通過GOT來實(shí)現(xiàn)變量的訪問爽室。當(dāng)共享模塊被裝載時(shí),如果某個(gè)全局變量在可執(zhí)行文件中擁有副本淆攻,那么動(dòng)態(tài)鏈接器就會(huì)把GOT中的相應(yīng)地址指向該副本阔墩,這樣該變量在運(yùn)行時(shí)實(shí)際上最終就只有一個(gè)實(shí)例。如果變量在共享模塊中被初始化瓶珊,那么動(dòng)態(tài)鏈接器還需要將該初始化值復(fù)制到主模塊中的變量副本啸箫;如果該全局變量在程序主模塊中沒有副本,那么GOT中的相應(yīng)地址就指向模塊內(nèi)部的該變量副本伞芹。
對(duì)于共享對(duì)象來說忘苛,如果數(shù)據(jù)段中有絕對(duì)地址引用,那么編譯器和鏈接器就會(huì)產(chǎn)生一個(gè)重定位表丑瞧,這個(gè)重定位表里面包含了“R_386_RELATIVE”類型的重定位入口柑土。當(dāng)動(dòng)態(tài)鏈接器裝載共享對(duì)象時(shí),如果發(fā)現(xiàn)該共享對(duì)象有這樣的重定位入口绊汹,那么動(dòng)態(tài)鏈接器就會(huì)對(duì)該共享對(duì)象進(jìn)行重定位稽屏。
我們?cè)诰幾g共享對(duì)象時(shí)如果使用“-fPIC”參數(shù),就表示要產(chǎn)生地址無關(guān)的代碼段西乖。GCC編譯動(dòng)態(tài)鏈接的可執(zhí)行文件會(huì)默認(rèn)帶上該參數(shù)的狐榔。如果不使用該參數(shù)就會(huì)產(chǎn)生一個(gè)裝載時(shí)重定位的共享對(duì)象坛增,它的代碼段就不是地址無關(guān)的,也就不能被多個(gè)進(jìn)程之間共享薄腻,于是就失去了節(jié)省內(nèi)存的優(yōu)點(diǎn)收捣。但是裝載時(shí)重定位的共享對(duì)象的運(yùn)行速度要比使用地址無關(guān)代碼的共享對(duì)象快,因?yàn)樗∪チ说刂窡o關(guān)代碼中每次訪問全局?jǐn)?shù)據(jù)和函數(shù)時(shí)需要做一次計(jì)算當(dāng)前地址以及間接地址尋址的過程庵楷。
動(dòng)態(tài)鏈接比靜態(tài)鏈接慢的主要原因是動(dòng)態(tài)鏈接下對(duì)于全局和靜態(tài)的數(shù)據(jù)訪問都要進(jìn)行復(fù)雜的GOT定位罢艾,然后間接尋址;對(duì)于模塊間的調(diào)用也要先定位GOT尽纽,然后再進(jìn)行間接跳轉(zhuǎn)咐蚯,這可能會(huì)導(dǎo)致程序啟動(dòng)或者運(yùn)行速度減慢,所以我們需要優(yōu)化動(dòng)態(tài)鏈接性能弄贿。
ELF采用延遲綁定來優(yōu)化動(dòng)態(tài)鏈接性能春锋,基本思想是當(dāng)函數(shù)第一次被用到時(shí)才進(jìn)行綁定(符號(hào)查找、重定位等)差凹。具體方法是使用了PLT(Procedure Linkage Table)期奔。PLT為GOT間接跳轉(zhuǎn)又增加了一個(gè)中間層,在調(diào)用某個(gè)外部模塊的函數(shù)時(shí)危尿,并不直接通過GOT跳轉(zhuǎn)呐萌,而是通過一個(gè)叫作PLT項(xiàng)的結(jié)構(gòu)來進(jìn)行跳轉(zhuǎn)。每個(gè)外部函數(shù)在PLT中都有一個(gè)相應(yīng)的項(xiàng)谊娇。(匯編指令實(shí)現(xiàn))
- 實(shí)際的PLT基本結(jié)構(gòu)代碼如下:
PLT0:
push *(GOT +4)
jump *(GOT+8)
...
bar@plt:
jmp *(bar@GOT)
push n
jump PLT0
在動(dòng)態(tài)鏈接情況下搁胆,操作系統(tǒng)在裝載完可執(zhí)行文件之后會(huì)先啟動(dòng)一個(gè)動(dòng)態(tài)鏈接器,之后就將控制權(quán)交給動(dòng)態(tài)鏈接器的入口地址邮绿。當(dāng)動(dòng)態(tài)鏈接器得到控制權(quán)之后,它開始執(zhí)行一系列自身的初始化操作攀例,然后根據(jù)當(dāng)前的環(huán)境參數(shù)船逮,開始對(duì)可執(zhí)行文件進(jìn)行動(dòng)態(tài)鏈接工作。當(dāng)所有動(dòng)態(tài)鏈接工作完成以后粤铭,動(dòng)態(tài)鏈接器會(huì)將控制權(quán)交到可執(zhí)行文件的入口地址挖胃,程序開始正式執(zhí)行。
-
動(dòng)態(tài)鏈接相關(guān)結(jié)構(gòu)
- “.interp”段:里面保存的就是一個(gè)字符串梆惯,這個(gè)字符串就是可執(zhí)行文件所需要的動(dòng)態(tài)鏈接器的路徑酱鸭。
- “.dynamic”段:ELF文件中最重要的結(jié)構(gòu),保存了依賴于哪些共享對(duì)象垛吗、動(dòng)態(tài)鏈接符號(hào)表的位置凹髓、動(dòng)態(tài)鏈接重定位表的位置、共享對(duì)象初始化代碼的地址等信息怯屉。
- “.dynsym”段:動(dòng)態(tài)符號(hào)表蔚舀,表示動(dòng)態(tài)鏈接模塊之間的符號(hào)導(dǎo)入導(dǎo)出關(guān)系饵沧。
- “.rel.dyn”段:數(shù)據(jù)引用重定位,修正“.got”以及數(shù)據(jù)段赌躺。
- “.rel.plt”段:函數(shù)引用重定位狼牺,修正“.got.plt”环凿。
動(dòng)態(tài)鏈接基本上分為3步:先是啟動(dòng)動(dòng)態(tài)鏈接器本身(自舉洁仗,bootstrap)猴娩,然后裝載所有需要的共享對(duì)象咖气,最后是重定位和初始化颜骤。(跳轉(zhuǎn))
完成基本自舉以后逆航,動(dòng)態(tài)鏈接器將可執(zhí)行文件和鏈接器本身的符號(hào)表都合并到一個(gè)全局符號(hào)表中尚卫。在Linux中遏匆,當(dāng)一個(gè)符號(hào)需要被加入全局符號(hào)表時(shí)痪署,如果相同的符號(hào)名已經(jīng)存在码泞,則后加入的符號(hào)被忽略(全局符號(hào)介入問題)。
當(dāng)上面的步驟完成之后狼犯,鏈接器開始重新遍歷可執(zhí)行文件和每個(gè)共享對(duì)象的重定位表余寥,將它們的GOT/PLT中的每個(gè)需要重定位的位置進(jìn)行修正。
Windows下的動(dòng)態(tài)鏈接
在ELF中悯森,由于代碼段是地址無關(guān)的宋舷,所以它可以實(shí)現(xiàn)多個(gè)進(jìn)程之間共享一份代碼,但是DLL的代碼卻并不是地址無關(guān)的瓢姻,所以它只是在某些情況下可以被多個(gè)進(jìn)程間共享祝蝠。
PE文件里有兩個(gè)常用的概念就是基地址(Base Address)和相對(duì)地址(RVA,Relative Virtual Address)幻碱∫锵粒基地址就是PE頭文件中的Image Base,是PE文件被裝載進(jìn)進(jìn)程地址空間中的起始地址褥傍,
對(duì)于EXE文件來說儡嘶,其值一般是0x400000,對(duì)于DLL文件來說恍风,其值一般是0x10000000蹦狂。而相對(duì)地址就是一個(gè)地址相對(duì)于基地址的偏移。ELF默認(rèn)導(dǎo)出所有的全局符號(hào)朋贬。但是在DLL中凯楔,我們需要顯式地“告訴”編譯器我們需要導(dǎo)出某個(gè)符號(hào),否則編譯器默認(rèn)所有符號(hào)都不導(dǎo)出锦募。在VC++中摆屯,我們使用“__declspec(dllexport)”表示DLL導(dǎo)出符號(hào),使用“__declspec(dllimport)”表示DLL導(dǎo)入符號(hào)御滩。除了使用導(dǎo)出導(dǎo)入符號(hào)外鸥拧,我們也可以使用“.def”文件中的IMPORT或者EXPORTS段來聲明導(dǎo)入導(dǎo)出符號(hào)党远。這個(gè)方法不僅對(duì)C/C++有效,對(duì)其他語言也有效富弦。
使用.def文件來描述DLL文件導(dǎo)出屬性的優(yōu)點(diǎn)有兩個(gè)沟娱,一是可以控制導(dǎo)出符號(hào)的符號(hào)名,而是可以控制一些鏈接的過程腕柜。
Windows提供3個(gè)API來支持DLL的運(yùn)行時(shí)鏈接济似,分別是LoadLibrary(LoadLibraryEx):裝載DLL,GetProcAddress:獲取某個(gè)符號(hào)的地址盏缤,F(xiàn)reeLibrary:卸載DLL砰蠢。
在Windows PE中,所有導(dǎo)出的符號(hào)被集中存放在導(dǎo)出表(Export Table)的結(jié)構(gòu)中唉铜。從最簡(jiǎn)單的結(jié)構(gòu)上來看台舱,它提供了一個(gè)符號(hào)名與符號(hào)地址的映射關(guān)系。導(dǎo)出表是一個(gè)IMAGE_EXPORT_DIRECTORY結(jié)構(gòu)體潭流,定義在“Winnt.h”中:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
導(dǎo)出表結(jié)構(gòu)中竞惋,最后3個(gè)成員執(zhí)行3個(gè)數(shù)組,分別是導(dǎo)出地址表(EAT灰嫉,Export Address Table)拆宛、符號(hào)名表(Name Table)和名字序號(hào)對(duì)應(yīng)表(Name-Ordinal Table)。
導(dǎo)出地址表中存放的是各個(gè)導(dǎo)出函數(shù)的RVA讼撒,符號(hào)名表中存放的是導(dǎo)出函數(shù)的名字浑厚。序號(hào)表實(shí)際是早期16位windows為了應(yīng)對(duì)內(nèi)存小而使用的機(jī)制。使用序號(hào)導(dǎo)入導(dǎo)出的好處就是省去了函數(shù)名查找過程根盒,函數(shù)名表也不需要保存到內(nèi)存中钳幅。但是它最大的問題就是一個(gè)函數(shù)的序號(hào)可能會(huì)變化。這就需要程序員手工指定每個(gè)導(dǎo)出函數(shù)的序號(hào)炎滞。由于目前硬件性能的提升贡这,這種內(nèi)存空間的節(jié)省和查找速度的提升效果就不明顯了。所以現(xiàn)在這種方式基本就不采用了厂榛,但是為了保持向后兼容,它還是被保留了下來丽惭。
動(dòng)態(tài)鏈接器如何查找函數(shù)RVA呢击奶?假設(shè)模塊A導(dǎo)入了Math.dll中的Add函數(shù),那么A的導(dǎo)入表中就保存了“Add”這個(gè)函數(shù)名责掏。當(dāng)進(jìn)行動(dòng)態(tài)鏈接時(shí)柜砾,動(dòng)態(tài)鏈接器在Math.dll的函數(shù)名表中進(jìn)行二分查找,找到“Add”函數(shù)换衬,然后在名字序號(hào)對(duì)應(yīng)表中找到“Add”所對(duì)應(yīng)的序號(hào)痰驱,即1证芭,減去Math.dll的Base值1,結(jié)果為0担映,然后在EAT中找到下標(biāo)0的元素废士,即“Add”的RVA為0x1000。
在ELF中蝇完,“.rel.dyn”和“.rel.plt”兩個(gè)段中分別保存了該模塊所需要導(dǎo)入的變量和函數(shù)的符號(hào)以及所在的模塊等信息官硝,而“.got”和“.got.plt”則保存著這些變量和函數(shù)的真正地址。Windows中也有類似機(jī)制短蜕,叫做導(dǎo)入表(Import Table)氢架。當(dāng)某個(gè)PE文件被加載時(shí),Windows加載器的其中一個(gè)任務(wù)就是將所有需要導(dǎo)入的函數(shù)地址確定并且將導(dǎo)入表中的元素調(diào)整到正確的地址朋魔,以實(shí)現(xiàn)動(dòng)態(tài)鏈接的過程岖研。
導(dǎo)入表是一個(gè)IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)體數(shù)組,每一個(gè)IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)對(duì)應(yīng)一個(gè)被導(dǎo)入的DLL警检。它也被定義在“Winnt.h”中:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
結(jié)構(gòu)體中的FirstThunk指向一個(gè)導(dǎo)入地址數(shù)組(IAT孙援,Import Address Table),IAT中每個(gè)元素對(duì)應(yīng)一個(gè)被導(dǎo)入的符號(hào)解滓,元素的值在不同的情況下有不同的含義赃磨。在動(dòng)態(tài)鏈接器剛完成映射還沒有開始重定位和符號(hào)解析時(shí),IAT中的元素值表示相對(duì)應(yīng)的導(dǎo)入符號(hào)的序號(hào)或者是符號(hào)名洼裤;當(dāng)Windows的動(dòng)態(tài)鏈接器在完成該模塊的鏈接時(shí)邻辉,元素值會(huì)被動(dòng)態(tài)鏈接器改寫成該符號(hào)的真正地址,從這一點(diǎn)看腮鞍,導(dǎo)入地址數(shù)組與ELF中的GOT非常類似值骇。(INT)
為了使得編譯器能夠區(qū)分函數(shù)是從外部導(dǎo)入的還是模塊內(nèi)部定義的,MSVC引入了“__declspec(dllimport)”的擴(kuò)展屬性移国,一旦一個(gè)函數(shù)被聲明為“__declspec(dllimport)”吱瘩,那么編譯器就知道它是外部導(dǎo)入的,以便產(chǎn)生相應(yīng)的指令形式迹缀。比如:CALL DWORD PTR [0x0040D11C]使碾。這里面的IAT表元素地址0x0040D11C也是絕對(duì)地址,這也是需要后面修正的祝懂。所以可以看到PE結(jié)構(gòu)中票摇,DLL的代碼段并非地址無關(guān)的,所以Windows系統(tǒng)就是大氣砚蓬,根本不像Linux那么在意代碼段指令的重復(fù)利用矢门。
因?yàn)镻E沒有類似ELF的全局符號(hào)介入問題,所以對(duì)于模塊內(nèi)部的全局函數(shù)調(diào)用,編譯器產(chǎn)生的都是直接調(diào)用指令CALL XXXXXXXX(不是相對(duì)地址偏移祟剔,是直接地址調(diào)用隔躲。這是因?yàn)閃indows PE下,任何一個(gè)PE文件在編譯時(shí)都會(huì)給出自己的一個(gè)優(yōu)先裝載位置物延,然后根據(jù)此位置產(chǎn)生一系列的定位宣旱,當(dāng)然這個(gè)絕對(duì)地址是需要在實(shí)際裝載運(yùn)行時(shí)再重新修正的,采用了一種重定基地址的方法)教届。
DLL優(yōu)化
DLL的代碼段和數(shù)據(jù)段本身并不是地址無關(guān)的响鹃,也就是說它默認(rèn)需要被裝載到由ImageBase指定的目標(biāo)地址中。如果目標(biāo)地址被占用案训,那么就需要裝載到其他地址买置,便會(huì)引起整個(gè)DLL的Rebase。這對(duì)于擁有大量DLL的程序來說强霎,頻繁的Rebase也會(huì)造成程序啟動(dòng)緩慢忿项。這是影響DLL性能的一個(gè)原因。
動(dòng)態(tài)鏈接過程中城舞,導(dǎo)入函數(shù)的符號(hào)在運(yùn)行時(shí)需要被逐個(gè)解析轩触。在這個(gè)解析過程中,免不了涉及到符號(hào)字符串的比較和查找過程家夺,這個(gè)查找過程中脱柱,動(dòng)態(tài)鏈接器會(huì)在目標(biāo)DLL的導(dǎo)出表中進(jìn)行符號(hào)字符串的二分查找。即使是使用了二分查找法拉馋,對(duì)于擁有DLL數(shù)量很多榨为,并且有大量導(dǎo)入導(dǎo)出符號(hào)的程序來說,這個(gè)過程仍然是非常耗時(shí)的煌茴。這是影響DLL性能的另一個(gè)原因随闺。
Windows PE采用了裝載時(shí)重定位來解決共享對(duì)象的地址沖突問題。這個(gè)重定位過程有些特殊蔓腐,因?yàn)樗羞@些需要重定位的地方只需要加上一個(gè)固定的差值矩乐,也就是說加上一個(gè)目標(biāo)裝載地址與實(shí)際裝載地址的差值。這主要得益于DLL內(nèi)部的地址都是基于基地址的回论,或者似乎相對(duì)于基地址的RVA散罕。所以這種重定位過程比一般的重定位要簡(jiǎn)單,速度更快一些傀蓉。PE里把這種特殊的重定位過程叫做重定基地址(Rebasing)笨使。
MSVC的鏈接器提供了指定輸出文件的基地址的功能×藕Γ可以在鏈接時(shí)使用link命令中的“/BASE”參數(shù)來指定基地址。比如:link /BASE:0x100100000, 0x10000 /DLL bar.obj。
Windows系統(tǒng)本身自帶很多系統(tǒng)的DLL萨蚕,基本上Windows的應(yīng)用程序運(yùn)行時(shí)都要用到靶草。Windows系統(tǒng)就在進(jìn)程空間中專門劃出一塊0x70000000~0x80000000區(qū)域,用于映射這些常用的系統(tǒng)DLL岳遥。Windows在安裝時(shí)就把這塊地址分配給這些DLL奕翔,調(diào)整這些DLL的基地址使得它們互相之間不沖突,從而在裝載時(shí)就不需要進(jìn)行重定基址了浩蓉。
每一次一個(gè)程序運(yùn)行時(shí)派继,所有被依賴的DLL都會(huì)被裝載,并且一系列的導(dǎo)入導(dǎo)出符號(hào)依賴關(guān)系都會(huì)被重新解析捻艳。在大多數(shù)情況下驾窟,這些DLL都會(huì)以同樣的順序被裝載到相同的內(nèi)存地址,所以它們的導(dǎo)出符號(hào)的地址應(yīng)該都是不變的认轨,既然這些符號(hào)的地址不變绅络,那程序主模塊的導(dǎo)入表應(yīng)該還是和上次程序運(yùn)行時(shí)相同,故而可以保留下來嘁字,這樣就可以省去每次啟動(dòng)時(shí)符號(hào)解析的過程恩急。這種方法稱為DLL綁定。
在PE的導(dǎo)入表中有一個(gè)和IAT一樣的數(shù)組叫做INT就是用來保存綁定符號(hào)的地址的纪蜒。一旦檢測(cè)到INT里面有信息衷恭,則不需要再次進(jìn)行符號(hào)重定位了,如果遇到問題(如依賴的DLL更新纯续,DLL裝載順序打亂了和此前裝載位置不一致)随珠,導(dǎo)致INT中綁定符號(hào)信息失效,則也可以依靠IAT的信息再重來一次重定位杆烁。Windows系統(tǒng)中很多系統(tǒng)自帶程序便采用DLL綁定用以加速程序啟動(dòng)牙丽。
參考文章
Windows下動(dòng)態(tài)鏈接之二:DLL優(yōu)化加速
如何理解DLL不是地址無關(guān)的?DLL與ELF的對(duì)比分析