第9章 Windows下的動(dòng)態(tài)鏈接

9.1

dll介紹

dll(Dynamic Link Library),動(dòng)態(tài)鏈接庫,它和exe基本上一樣,只不過它的pe文件頭中的符號(hào)表標(biāo)明該文件是dll而不是exe。

dll文件的后綴名不僅是dll還可以是ocx和cpl。

dll設(shè)計(jì)的目的不同于共享對(duì)象,它關(guān)注的是軟件工程中的模塊化設(shè)計(jì)思想,想要做到高內(nèi)聚低耦合,它方便了軟件的升級(jí)和維護(hù)。

9.1.2基地址和RVA

exe文件里面也有基地址和RVA(Relative Virtual Address,相對(duì)虛擬地址)。

對(duì)于dll來說它的基地址有一個(gè)默認(rèn)值,如果這個(gè)默認(rèn)值被其他模塊占用了,它就找個(gè)別的地方裝載。

9.1.3

dll共享數(shù)據(jù)段

dll中有些數(shù)據(jù)是共享的,有些是進(jìn)程私有的。

按照這個(gè)區(qū)別劃分的話,就可以把數(shù)據(jù)段分成兩種。

不過由于各進(jìn)程共同訪問公共數(shù)據(jù)段,如果某一進(jìn)程惡意破壞數(shù)據(jù),其他進(jìn)程也會(huì)受到影響,這存在一定的安全隱患。

9.1.4

dll的簡單例子

ELF中的符號(hào)默認(rèn)都是可以導(dǎo)入導(dǎo)出的,所謂導(dǎo)出就是指可以被別的模塊調(diào)用,導(dǎo)入就不解釋了,而DLL中的符號(hào)需要指定才能導(dǎo)入導(dǎo)出。

__declspec(dllexport)用來指定導(dǎo)出符號(hào),__declspec(dllimport)用來指定導(dǎo)入符號(hào)。

另外,還可以用def文件中的IMPORT和EXPORTS段來聲明符號(hào)的導(dǎo)入和導(dǎo)出。

如果是C語言的符號(hào)規(guī)范,你必須在符號(hào)的定義之前加上external

“C”。

9.1.7使用模塊定義文件

就是前面提到的def文件了,它的好處有如下幾點(diǎn):

1、能夠控制導(dǎo)出符號(hào)的符號(hào)名。原來__cdecl、__stdcall、__fastcall都是msvc中的函數(shù)規(guī)范啊。編譯器會(huì)對(duì)源碼中的符號(hào)進(jìn)行修飾,經(jīng)過修飾的符號(hào)變得和環(huán)境中的符號(hào)不兼容,不便于維護(hù)和使用,于是采用def文件對(duì)導(dǎo)出符號(hào)進(jìn)行重命名。

2、它可以控制一些鏈接的過程。它還可以控制輸出文件名、段的屬性、堆棧大小、版本號(hào)等。

9.1.8

DLL顯示運(yùn)行時(shí)鏈接

即,運(yùn)行時(shí)加載。

Windows提供了3個(gè)API:

1、LoadLibrary裝載一個(gè)DLL進(jìn)進(jìn)程地址空間。

2、GetProAddress查找某個(gè)符號(hào)的地址。

3、FreeLibrary用來卸載某個(gè)已經(jīng)加載的模塊。

9.2符號(hào)導(dǎo)出導(dǎo)入表

9.2.1導(dǎo)出表

Windows下的PE文件的導(dǎo)出符號(hào)全部集中在導(dǎo)出表中,供其他PE文件調(diào)用,它提供的是一種符號(hào)與地址的映射關(guān)系。

導(dǎo)出表是個(gè)DataDirectory的結(jié)構(gòu)體數(shù)組,名字叫做IMAGE_EXPORT_DIRECTORY,被定義在Winnt.h中。

DataDirectory中最后三項(xiàng)EAT(Export Address Table)、Name

Table、Name

Ordinal Table分別代表導(dǎo)出地址表、符號(hào)名表、名字序號(hào)對(duì)應(yīng)表。

導(dǎo)出地址表存放的是個(gè)符號(hào)的相對(duì)虛擬地址。

符號(hào)名表存放的是導(dǎo)出符號(hào)的名字。

名字序號(hào)對(duì)應(yīng)表存放的是函數(shù)的序號(hào)和函數(shù)名的對(duì)應(yīng)關(guān)系。

函數(shù)序號(hào)存在的意義在于節(jié)省空間和查找方便,壞處是函數(shù)變化了序號(hào)也要跟著變化。導(dǎo)出函數(shù)一定有序號(hào)但可以沒有函數(shù)名。

9.2.2

EXP文件

在創(chuàng)建DLL時(shí)會(huì)產(chǎn)生一個(gè)EXP,EXP中的.edata存放的是導(dǎo)出表。EXP會(huì)與其他目標(biāo)文件一樣一起鏈接生成DLL并且成為導(dǎo)出表。

9.2.4導(dǎo)入表

來自于DLL和其他可執(zhí)行文件中的符號(hào)會(huì)存儲(chǔ)在導(dǎo)入表中。

導(dǎo)入表是一個(gè)IMAGE_EXPORT_DIRECTORY結(jié)構(gòu)體數(shù)組。

IMAGE_EXPORT_DIRECTORY中的FirstThunk指向?qū)氲刂窋?shù)組(Import

Address Table,IAT),每個(gè)IAT對(duì)應(yīng)一個(gè)被導(dǎo)入的符號(hào)。

延遲載入:DLL也支持延遲裝載,它是通過特殊的樁代碼實(shí)現(xiàn)的。

9.2.5導(dǎo)入函數(shù)的調(diào)用

PE DLL的代碼段并不是地址無關(guān)的。

PE采用了重定基地址的方法來解決模塊裝載時(shí)進(jìn)程空間中地址沖突的問題。

__declspec(dllimport)的作用是使編譯器能夠區(qū)分函數(shù)是從外部導(dǎo)入的還是模塊內(nèi)部定義的。

同一個(gè)導(dǎo)出函數(shù)會(huì)產(chǎn)生兩個(gè)符號(hào)的定義,一個(gè)指向該函數(shù)的樁代碼,一個(gè)指向該函數(shù)在IAT中的位置。

用__declspec(dllimport)來聲明導(dǎo)入函數(shù)時(shí)會(huì)在導(dǎo)入函數(shù)前面加上__imp__以確保跟庫中的函數(shù)符號(hào)正確鏈接。

9.3

DLL優(yōu)化

DLL本身的代碼段和數(shù)據(jù)段并不是地址無關(guān)的。

一旦DLL的基址被占用,它就必須被重定位,這需要時(shí)間開銷。

雖然在DLL中采用二分查找法進(jìn)行符號(hào)字符串的比較和查找,但是由于符號(hào)眾多,因此這也是一項(xiàng)非常耗時(shí)的工作。

以上是影響DLL性能的兩個(gè)問題。

9.3.1重定基地址

在裝載DLL時(shí)發(fā)生了地址沖突,就必須對(duì)每個(gè)絕對(duì)地址的引用都進(jìn)行重定位。

重定位的過程很簡單就是在原來的地址基礎(chǔ)上加上一個(gè)偏移量,但是這是對(duì)所有需要重定位的絕對(duì)地址而言的。

PE文件的重定位信息都放在reloc段中。

一般來講exe文件是不會(huì)發(fā)生重定位的,因?yàn)樗偸潜坏谝粋€(gè)裝載。

而DLL則是動(dòng)態(tài)裝載所以它的裝載地址可能被占用。

DLL文件中代碼段的訪問比ELF更加快速,是一種空間換時(shí)間的優(yōu)化策略,因?yàn)樗總€(gè)進(jìn)程都有一個(gè)副本。

確切地說它是裝載時(shí)重定位基址。

DLL的裝載和地址順序是一樣的。

DLL的基地址是可以手動(dòng)指定的,不然老是重定位多麻煩啊,VC提供這種功能。

9.3.2序號(hào)

一個(gè)導(dǎo)出的函數(shù)符號(hào)可以沒有函數(shù)名,但是絕對(duì)不能沒有序號(hào)。

序號(hào)表示導(dǎo)出函數(shù)在導(dǎo)出表中的位置。

內(nèi)部使用的函數(shù)一般只有序號(hào)沒有函數(shù)名。

Windows API雖然函數(shù)名是不變的,但是序號(hào)總是在變化的。

序號(hào)可以通過def文件指定。

凡是涉及到PE文件的查找的地方用的都是二分查找法。

9.3.3導(dǎo)入函數(shù)綁定

由于無論如何導(dǎo)入導(dǎo)出的符號(hào)關(guān)系都會(huì)被重新解析,而且每次解析完畢后它們被裝載的內(nèi)存地址都相同,那么這個(gè)解析過程就是浪費(fèi)。

針對(duì)這種浪費(fèi)采取的優(yōu)化策略就叫做DLL綁定,具體方法是把這些導(dǎo)入的符號(hào)保存在模塊的導(dǎo)入表中每次只需查表即可。

INT(Import Name Table),導(dǎo)入名稱表,把符號(hào)運(yùn)行時(shí)的目標(biāo)地址寫到INT中。

DLL更新和重定基址可能導(dǎo)致DLL綁定地址失效。

針對(duì)DLL更新,Windows會(huì)核對(duì)裝載的DLL與綁定時(shí)的DLL版本是否相同,還有該DLL是否發(fā)生過重定基址,如果都沒有那就直接查表。

DLL綁定過程可以發(fā)生在安裝的時(shí)候,可能改變可執(zhí)行文件本身從而導(dǎo)致可執(zhí)行文件的校驗(yàn)和變化。這對(duì)于一些經(jīng)過加密的和數(shù)字簽名的程序來說可能會(huì)有問題。

9.4

C++與動(dòng)態(tài)鏈接

Linux下的共享庫絕大多數(shù)都是用C語言寫的,這是因?yàn)镃++編寫的庫比C語言編寫的庫要復(fù)雜得多,而且由于C++沒有二進(jìn)制級(jí)別的規(guī)定只有語法級(jí)別的,所以也為共享庫的更新帶來了不便。

C++帶來的麻煩主要表現(xiàn)在以下幾個(gè)方面:

1、內(nèi)存釋放過程復(fù)雜,不好把握。因?yàn)椴煌腄LL和EXE使用不同的堆。

2、所謂的更新只不過是簡單的覆蓋。

3、正是由于舊版DLL被覆蓋,如果新版程序運(yùn)行出錯(cuò),舊版程序也運(yùn)行出錯(cuò),那就悲劇了。

為了解決程序開發(fā)中遇到的兼容性問題,微軟推出了組件對(duì)象模型(COM,Component

Object Model)。

P300列出了幾點(diǎn)用C++編寫DLL時(shí)應(yīng)該遵循的原則。

9.5

DLL HELL

早期Windows中的DLL文件使用范圍大,更新也頻繁,而且還缺乏版本控制機(jī)制,所以DLL不兼容的情況在早期Windows下特別嚴(yán)重,史稱DLL噩夢(mèng)。

導(dǎo)致DLL

HELL發(fā)生的3個(gè)原因見P301中所說明的。

解決DLL

HELL的方法:

1、由于DLL HELL是由DLL引起的,即動(dòng)態(tài)鏈接引起的,所以最徹底的解決辦法是不使用動(dòng)態(tài)鏈接,而使用靜態(tài)鏈接。

2、避免DLL覆蓋。這個(gè)可以通過Windows的文件保護(hù)機(jī)制實(shí)現(xiàn)。

3、避免DLL沖突。它主要是針對(duì)不同應(yīng)用程序依賴相同DLL的不同版本的問題,解決辦法是讓每個(gè)應(yīng)用程序。

.NET下的程序集包括兩種類型應(yīng)用程序集和庫程序集,前者是指EXE可執(zhí)行文件,后者是指DLL動(dòng)態(tài)鏈接庫。

由于程序集包括一個(gè)或多個(gè)文件所需要一個(gè)清單來描述,這個(gè)清單叫做Manifest文件,它就是描述了程序及的各種屬性信息,其本質(zhì)是一個(gè)XML文件。

在Windows

XP以前的版本中Manifest就是個(gè)擺設(shè),這以后的Windows版本在執(zhí)行可執(zhí)行文件時(shí)首先要讀取Manifest內(nèi)容獲取所需的DLL文件列表,然后Windows再根據(jù)DLL的Manifest文件去掉用DLL。

CRT(C Run-Time Library):C語言運(yùn)行庫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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