《程序員的自我修養(yǎng)》筆記(二)——裝載與動(dòng)態(tài)鏈接

裝載與動(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ì)比分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兔魂,一起剝皮案震驚了整個(gè)濱河市烤芦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌析校,老刑警劉巖构罗,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異智玻,居然都是意外死亡遂唧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吊奢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盖彭,“玉大人,你說我怎么就攤上這事≌俦撸” “怎么了铺呵?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)隧熙。 經(jīng)常有香客問我片挂,道長(zhǎng),這世上最難降的妖魔是什么贞盯? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任音念,我火速辦了婚禮,結(jié)果婚禮上躏敢,老公的妹妹穿的比我還像新娘闷愤。我一直安慰自己,他們只是感情好父丰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布肝谭。 她就那樣靜靜地躺著,像睡著了一般蛾扇。 火紅的嫁衣襯著肌膚如雪攘烛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天镀首,我揣著相機(jī)與錄音坟漱,去河邊找鬼。 笑死更哄,一個(gè)胖子當(dāng)著我的面吹牛芋齿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播成翩,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼觅捆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了麻敌?” 一聲冷哼從身側(cè)響起栅炒,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎术羔,沒想到半個(gè)月后赢赊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡级历,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年释移,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寥殖。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡玩讳,死狀恐怖涩蜘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熏纯,我是刑警寧澤皱坛,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站豆巨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏掐场。R本人自食惡果不足惜往扔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熊户。 院中可真熱鬧萍膛,春花似錦、人聲如沸嚷堡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蝌戒。三九已至串塑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間北苟,已是汗流浹背桩匪。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留友鼻,地道東北人傻昙。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像彩扔,于是被迫代替她去往敵國(guó)和親妆档。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容