本篇文章將介紹DLL顯式鏈接的過程和模塊基地址重定位及模塊綁定的技術(shù)。
第一種將DLL映射到進(jìn)程地址空間的方式是直接在源代碼中引用DLL中所包含的函數(shù)或是變量,DLL在程序運(yùn)行后由加載程序隱式的載入,此種方式被稱為隱式鏈接。
第二種方式是在程序運(yùn)行時(shí),通過調(diào)用API顯式的載入所需要的DLL,并顯式的鏈接所想要鏈接的符號。換句話說,程序在運(yùn)行時(shí),其中的一個(gè)線程能夠顯式的將該DLL調(diào)用到進(jìn)程地址空間中,并得到DLL中某函數(shù)的在進(jìn)程地址空間的虛擬地址,然后調(diào)用該函數(shù)。此種方式被稱為顯式鏈接。
注意:顯式載入某DLL時(shí),不需要該dll的Lib文件,且exe文件中并不包含該dll的導(dǎo)入表。
顯示載入DLL模塊的步驟:
線程可以調(diào)用LoadLibrary將一個(gè)DLL映射到進(jìn)程地址空間。
該函數(shù)會(huì)試圖對程序想載入的DLL進(jìn)行定位,并試圖將該DLL映射到調(diào)用進(jìn)程的地址空間中。返回是DLL在調(diào)用進(jìn)程的虛擬地址。即模塊的句柄。如果無法將DLL載入到進(jìn)程地址空間中返回值為NULL.
與它類似的另一個(gè)函數(shù)
也可以實(shí)現(xiàn)將DLL載入到進(jìn)程地址空間的目的。具體請參考MSDN。
加載后如果程序不再需要該DLL,可以調(diào)用FreeLibrary將DLL從進(jìn)程地址空間中卸載:
也可以調(diào)用FreeLibraryEx卸載某DLL。
以下函數(shù)不僅具有從進(jìn)程地址空間卸載某DLL的功能,還能退出調(diào)用線程:
剛見到時(shí)或許你會(huì)覺得它很多余??紤]下面的情形:
我們調(diào)用一個(gè)DLL,該DLL中的代碼會(huì)創(chuàng)建一個(gè)線程,當(dāng)此線程完成工作后,可以調(diào)用FreeLibrary和ExitThread將DLL從進(jìn)程地址空間中卸載,并終止自己。由于線程是由DLL創(chuàng)建的,線程執(zhí)行的代碼也在DLL中,當(dāng)線程調(diào)用FreeLibrary將它所在的DLL卸載的時(shí)候,它后續(xù)要執(zhí)行的代碼已不再進(jìn)程地址空間中了,試圖執(zhí)行不存在的代碼可能會(huì)導(dǎo)致訪問違規(guī),導(dǎo)致進(jìn)程被終止。
如果線程調(diào)用FreeLibraryAndExitThread,此函數(shù)在Kernel32.dll中,F(xiàn)reeLibraryAndExitThread函數(shù)調(diào)用FreeLibrary將線程函數(shù)所在的DLL卸載后,其所屬DLL Kernel32.dll仍在進(jìn)程地址空間內(nèi),F(xiàn)reeLibraryAndExitThread函數(shù)繼續(xù)執(zhí)行調(diào)用ExitThread,后續(xù)代碼仍然存在,不會(huì)導(dǎo)致訪問違規(guī)。
每個(gè)DLL在進(jìn)程中都有一個(gè)使用計(jì)數(shù)。LoadLibrary(Ex)會(huì)增加其計(jì)數(shù),FreeLibrary(Ex)和FreeLibraryAndExitThread會(huì)遞減其計(jì)數(shù)。例如:當(dāng)程序第一次調(diào)用LoadLibrary來載入一個(gè)DLL時(shí),系統(tǒng)會(huì)將此DLL映射到進(jìn)程地址空間中,并將此DLL的使用計(jì)數(shù)加一。如果線程后來再次調(diào)用LoadLibrary(Ex)時(shí),系統(tǒng)不會(huì)將此DLL再次映射到進(jìn)程地址空間,僅僅遞增此DLL的使用計(jì)數(shù)。為了從進(jìn)程地址空間中撤銷對該DLL的映射,線程必須調(diào)用FreeLibrary(Ex)兩次。第一次是將此DLL的使用計(jì)數(shù)減為1,第二次減為0。當(dāng)系統(tǒng)發(fā)現(xiàn)某DLL的使用計(jì)數(shù)已經(jīng)為0時(shí),會(huì)從進(jìn)程地址空間卸載此DLL。此時(shí)如果線程試圖顯式調(diào)用DLL中的函數(shù)將會(huì)導(dǎo)致訪問違規(guī)。
系統(tǒng)會(huì)在每個(gè)進(jìn)程中為DLL維護(hù)一個(gè)使用計(jì)數(shù),在本進(jìn)程調(diào)用LoadLibrary僅僅是增加DLL在本進(jìn)程的使用計(jì)數(shù)。如果進(jìn)程A中的一個(gè)線程執(zhí)行了LoadLibrary("Mydll.dll");進(jìn)程B的某一線程也調(diào)用LoadLibrary("Mydll.dll");那么該DLL會(huì)被映射到A,B兩個(gè)進(jìn)程空間中去,且在A和B進(jìn)程的使用計(jì)數(shù)都為1。
調(diào)用FreeLibrary("Mydll.dll");也僅僅是遞減DLL在本進(jìn)程內(nèi)的使用計(jì)數(shù)。
該函數(shù)可以用來檢測某DLL是否被映射到了進(jìn)程地址空間。如果返回值為NULL,則此DLL未被載入。
當(dāng)給pszModuleName傳NULL時(shí),函數(shù)會(huì)返回應(yīng)用程序可執(zhí)行文件的句柄。
顯式鏈接導(dǎo)出符號
顯式載入某個(gè)DLL后,線程可以通過調(diào)用以下函數(shù)來得到它要引用的符號的地址。
hInstDll標(biāo)識導(dǎo)出符號所在的DLL的句柄。它是LoadLibrary(Ex),或是GetModuleHandle所返回的句柄。
pszSymbolName用于標(biāo)識導(dǎo)出符號。
pszSymbolName可以有兩種形式:
第一種:用符號名來指定我們想要得到哪個(gè)符號的地址。
如:FARPROC pfn=GetProcAddress(hInstDll,"MyProc");
它是以0結(jié)尾的字符串。要注意此字符串是ANSI類型的。因?yàn)榫幾g器、鏈接器始終都是將符號的名稱以ANSI字符串的形式保存在DLL的導(dǎo)出段。
第二種:用序號來指定我們想要那個(gè)符號的地址。
如:FARPROC pfn=GetProcAddress(hInstDll,MAKERESOURCE(2));
這種方法假定我們知道某個(gè)導(dǎo)出符號在某DLL中的序號為2。應(yīng)該明確的是Microsoft強(qiáng)烈反對使用序號。
使用序號的形式要比使用字符串速度慢,因?yàn)橄到y(tǒng)需要對一字符串標(biāo)識的符號名進(jìn)行字符串比較。使用第二種方法即使該序號并沒有與任何導(dǎo)出函數(shù)相對應(yīng),GetProcAddress也會(huì)返回非NULL值。其實(shí)這個(gè)地址是無效的,訪問此地址可能會(huì)導(dǎo)致訪問違規(guī)。
注意:使用GetProcAddress返回的函數(shù)指針來調(diào)用函數(shù)之前,需要將它轉(zhuǎn)換成與函數(shù)簽名相匹配的類型。
例如:
它是與void DynamicDumpModule(HMODULE hModule)函數(shù)相對應(yīng)的函數(shù)相同。
動(dòng)態(tài)調(diào)用某DLL導(dǎo)出函數(shù)的例子:
DLL的入口點(diǎn)函數(shù)
一個(gè)DLL可以有一個(gè)入口點(diǎn)函數(shù),系統(tǒng)會(huì)在不同的時(shí)候調(diào)用這個(gè)函數(shù)。這些調(diào)用是通知性質(zhì)的,通常被DLL用來執(zhí)行與進(jìn)程或線程有關(guān)的初始化和清理工作。
如果不需要執(zhí)行這些操作,可以不必再源代碼中不實(shí)現(xiàn)此函數(shù)。
如果需要DLL接受這些通知,就應(yīng)該按照如下的格式來實(shí)現(xiàn)該函數(shù)。
hInstDll是該DLL實(shí)例的句柄。它是DLL文件被映射到進(jìn)程地址空間的虛擬地址。通常將這個(gè)參數(shù)保存在全局變量中。這樣在DLL的其他導(dǎo)出函數(shù)中就可以使用。
如果DLL是被隱式載入的,fImpLoad為非零值,顯式的話fImportLoad為0。
fdwReason表示系統(tǒng)調(diào)用入口點(diǎn)函數(shù)的原因。它是switch語句的參數(shù)??梢允巧鲜鏊膫€(gè)值。分別表示四種情況。后續(xù)將會(huì)詳細(xì)介紹每一種情況。
注意:DLL使用DllMain對自己進(jìn)行初始化。DllMain執(zhí)行的時(shí)候,其他DLL的可能還未被初始化。這意味著我們應(yīng)該避免在DllMain中調(diào)用從其他DLL中導(dǎo)出的函數(shù)。
DLL_PROCESS_ATTACH通知
當(dāng)系統(tǒng)第一次將一個(gè)DLL映射到進(jìn)程地址空間是,會(huì)調(diào)用DllMain函數(shù),并給fdwReason傳入DLL_PROCESS_ATTACH。注意:只有在該DLL是第一次被調(diào)用到進(jìn)程地址空間中時(shí),才會(huì)調(diào)用DllMain。如果以后再次調(diào)用LoadLibrary(Ex)時(shí),OS僅僅是遞增該DLL在此進(jìn)程的使用計(jì)數(shù),并不會(huì)再次調(diào)用DllMain。
當(dāng)DLL在處理DLL_PROCESS_ATTACH時(shí),應(yīng)該根據(jù)需要執(zhí)行與進(jìn)程相關(guān)的初始化。如DLL中包含一些函數(shù),需要使用自己的堆,可以在進(jìn)程加載時(shí)執(zhí)行一些堆的初始化工作。
處理DLL_PROCESS_ATTACH時(shí),DllMain的返回值表示DLL的初始化是否成功。如初始化成功,應(yīng)返回TRUE,否則應(yīng)返回false。
下面來看看DllMain調(diào)用的時(shí)機(jī):
創(chuàng)建新進(jìn)程時(shí),系統(tǒng)為該進(jìn)程分配地址空間,并將exe可執(zhí)行文件和所需要的DLL映射到進(jìn)程地址空間。然后創(chuàng)建主線程,并用主線程來調(diào)用每個(gè)DLL的DllMain函數(shù),同時(shí)傳入DLL_PROCESS_ATTACH。當(dāng)所有已映射的DLL完成對該通知的處理后,系統(tǒng)會(huì)讓主進(jìn)程執(zhí)行可執(zhí)行模塊的C/C++運(yùn)行庫的啟動(dòng)代碼。然后執(zhí)行可執(zhí)行模塊的入口點(diǎn)函數(shù)(_tmain或_tWinMain)。如果任意一個(gè)DLL的DllMain返回false,就說明初始化失敗,系統(tǒng)會(huì)將所有文件映像從地址空間中清除,向用戶顯示錯(cuò)誤信息。
顯式載入DLL的過程:
進(jìn)程調(diào)用LoadLibrary(Ex),該函數(shù)對DLL進(jìn)行定位,并將該DLL映射到進(jìn)程地址空間。然后會(huì)讓調(diào)用LoadLibrary(Ex)的線程調(diào)用DllMain函數(shù),并傳入DLL_PROCESS_ATTACH。當(dāng)DLL的DllMain函數(shù)完成了對通知的處理后,系統(tǒng)會(huì)讓LoadLibrary返回。這樣線程就可以繼續(xù)執(zhí)行。
注意:DllMain是在進(jìn)程調(diào)用LoadLibrary(Ex)的時(shí)候調(diào)用的。它返回到LoadLibrary(Ex)函數(shù)內(nèi)。
DLL_PROCESS_ATTACH通知
當(dāng)一個(gè)DLL從進(jìn)程的地址空間中撤銷的時(shí)候,會(huì)調(diào)用該DLL的DllMain函數(shù),并在fdwReason傳入DLL_PROCESS_DETACH。該case語句內(nèi)一般是用來執(zhí)行與進(jìn)程相關(guān)的清理工作。如調(diào)用HeapDestroy清理堆。
注意:當(dāng)DLL剛被映射到進(jìn)程地址空間,執(zhí)行DllMain并傳入DLL_PROCESS_ATTACH時(shí)的返回值為false時(shí),所有DLL將會(huì)被撤銷映射,此時(shí)并不會(huì)調(diào)用DllMain并傳入DLL_PROCESS_DETACH。
下面談?wù)務(wù){(diào)用DllMain并傳入DLL_PROCESS_DETACH的時(shí)機(jī):
1:當(dāng)進(jìn)程又由于某線程調(diào)用ExitProcess而終止時(shí),映射到該進(jìn)程的所有DLL都會(huì)被撤銷。調(diào)用 ExitProcess的線程將負(fù)責(zé)執(zhí)行DllMain。一般情況下,此線程就是主線程。
2:如果DLL被撤銷的原因是因?yàn)檫M(jìn)程中的線程調(diào)用了FreeLibrary或是FreeLibraryAndExitThread,那么執(zhí)行上述函數(shù)的線程將負(fù)責(zé)對DllMain的調(diào)用。調(diào)用完成后線程返回,繼續(xù)執(zhí)行其他代碼。
注意:如果進(jìn)程終止是因?yàn)槟硞€(gè)線程調(diào)用TerminateProcess,此時(shí)DllMain并不會(huì)被調(diào)用。這意味著在進(jìn)程終止前,已經(jīng)映射到進(jìn)程的任何DLL將沒有任何機(jī)會(huì)執(zhí)行清理工作,這可能導(dǎo)致數(shù)據(jù)丟失或是已被該進(jìn)程占用的信號量不能得到釋放。因此不到萬不得已,應(yīng)該避免使用TerminageProcess。
DLL_THREAD_DEATTACH通知
當(dāng)進(jìn)程創(chuàng)建一線程的時(shí)候,系統(tǒng)會(huì)檢查已映射到此進(jìn)程的所有DLL,并用DLL_THREAD_ATTACH調(diào)用每個(gè)DLL的DllMain。一般在此時(shí)執(zhí)行與線程有關(guān)的初始化。DllMain的代碼是由新創(chuàng)建的線程執(zhí)行。當(dāng)該線程完成了所有DllMain之后,才會(huì)執(zhí)行它的線程函數(shù)。
注意:僅僅是讓新建的線程執(zhí)行已經(jīng)被映射到進(jìn)程地址空間的DLL的DllMain函數(shù)。而不會(huì)讓已經(jīng)存在的線程調(diào)用DllMain。當(dāng)系統(tǒng)的主線程被創(chuàng)建的時(shí)候,并不會(huì)調(diào)用DllMain并傳入DLL_PROCESS_ATTACH。它已經(jīng)在進(jìn)程被創(chuàng)建的時(shí)候調(diào)用DllMain并傳入DLL_PROCESS_ATTACH。
DLL_THREAD_DETACH通知
當(dāng)線程調(diào)用ExitThread將要終止的時(shí)候,系統(tǒng)會(huì)讓該線程用DLL_THREAD_DETACH調(diào)用所有已映射到進(jìn)程地址空間的所有DLL的DllMain函數(shù)。這一般被用來執(zhí)行與線程相關(guān)的清理工作。
注意:如果線程終止是因?yàn)槠渌€程調(diào)用TerminateThread而終止的話,系統(tǒng)不會(huì)用DLL_THREAD_DETACH讓線程調(diào)用各DLL的DllMain。因此與TerminateProcess一樣,除非萬不得以,應(yīng)避免使用TerminateThread函數(shù)。
下面來總結(jié)下調(diào)用DllMain的過程:
進(jìn)程中的一個(gè)線程調(diào)用LoadLibrary來映射一個(gè)DLL,系統(tǒng)使該線程用DLL_PROCESS_ATTCH調(diào)用該DLL的DllMain函數(shù)(該線程不會(huì)得到DLL_THREAD_ATTACH)通知。當(dāng)此線程退出時(shí),系統(tǒng)讓此線程再次調(diào)用所有DLL的DllMain函數(shù),但此次傳入的是DLL_THREAD_DETACH。雖然在該DLL映射的時(shí)候,不會(huì)向該DLL發(fā)送DLL_THREAD_ATTACH通知。但是當(dāng)該線程退出時(shí),會(huì)向DLL發(fā)送DLL_THREAD_DETACH通知。
之所以不發(fā)送DLL_PROCESS_DETACH通知,是因?yàn)镈LL仍在進(jìn)程中。只有當(dāng)DLL被卸載時(shí),才會(huì)發(fā)送此通知。
前面我們提到過DllMain函數(shù)并不是必須的。在鏈接DLL的時(shí)候,如果鏈接器無法在obj文件中發(fā)現(xiàn)DllMain函數(shù),它會(huì)鏈接C/C++運(yùn)行庫的DllMain函數(shù)。如果我們不提供DllMain函數(shù),C/C++運(yùn)行庫會(huì)認(rèn)為我們不關(guān)系DLL的各種通知。它會(huì)調(diào)用DisableThreadLibraryCalls函數(shù)。
該函數(shù)告訴系統(tǒng) 我們不想讓系統(tǒng)向某個(gè)指定的DLL發(fā)送DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
C/C++運(yùn)行庫中實(shí)現(xiàn)的DllMain函數(shù)如下所示:
延遲載入DLL。
所謂延遲載入DLL,就是在進(jìn)程運(yùn)行后加載程序加載各種DLL時(shí),并不載入已經(jīng)被設(shè)為延遲加載的DLL。直到該DLL中的某個(gè)導(dǎo)出函數(shù)被調(diào)用的時(shí)候,此DLL才會(huì)被加載到進(jìn)程的地址空間中。該DLL是隱式鏈接的 。
這項(xiàng)特性非常有用,主要應(yīng)用與以下各種情況下:
1:某進(jìn)程使用了很多DLL,由于初始化時(shí)加載程序必須將所有DLL都映射到進(jìn)程地址空間中,這會(huì)導(dǎo)致加載速度比較慢。如果使用延遲加載,某些DLL直到其導(dǎo)出符號被引用到的時(shí)候,該DLL才會(huì)被隱式加載到進(jìn)程地址空間,這縮短了初始化時(shí)間。
2:當(dāng)應(yīng)用程序在代碼上使用了一個(gè)新的函數(shù),運(yùn)行在不提供此函數(shù)的老版本的系統(tǒng)上時(shí),如果該函數(shù)所在的DLL不使用延遲加載機(jī)制,加載程序會(huì)報(bào)告一個(gè)錯(cuò)誤:無法找到該函數(shù)。接著便會(huì)終止該應(yīng)用程序的執(zhí)行。如果我們使用延遲加載技術(shù),當(dāng)程序檢測到此時(shí)是運(yùn)行在老的系統(tǒng)中,程序就不會(huì)調(diào)用此函數(shù),轉(zhuǎn)而使用可以在老的系統(tǒng)上使用的其它函數(shù)。程序仍然可以繼續(xù)運(yùn)行。由于不會(huì)在程序中引用在老系統(tǒng)中不支持的函數(shù),該函數(shù)所在的DLL就不會(huì)被加載。
當(dāng)然任何方法都有適用范圍,延遲加載不適用于以下幾種情況:
1:導(dǎo)出全局變量的DLL是無法延遲加載的。
2:Kernel32.dll是無法延遲加載的,LoadLibrary和GetProcAddress都在該模塊中。必須加載該模塊才可以調(diào)用它們。
3:不應(yīng)該在DllMain中代用延遲加載函數(shù),這樣會(huì)導(dǎo)致程序崩潰。
要讓延遲加載能夠正常工作,首先要指定兩個(gè)鏈接器開關(guān)。
/Lib:DelayImp.dl
/DelayLoad:要延遲加載的DLL名字。
它們不可以在代碼中通過#pragma comment(linker,"")來設(shè)定。而要通過Configuration Properities屬性頁來設(shè)定。
/Lib:DelayImp.dll是通過Linker/Advanced/DelayLoadDLL開關(guān)來指定。它告訴鏈接器將函數(shù)_delay_LoadHelper2嵌入到我們的可執(zhí)行文件中。
/DelayLoad開關(guān)可以通過Linker /input/DelayLoadDLLs開關(guān)來指定。要延遲載入的函數(shù)所在的DLL在該項(xiàng)的右側(cè)指定??梢灾付ǘ鄠€(gè)延遲載入DLL。
該開關(guān)告訴鏈接器::
1:將用戶要延遲載入的DLL從可執(zhí)行文件的導(dǎo)入段中去除,這樣當(dāng)進(jìn)程初始化時(shí)該DLL就不會(huì)被隱式的載入。
2:在可執(zhí)行文件中嵌入一個(gè)延遲載入段,來表示要從用戶要延遲載入的DLL導(dǎo)入哪些函數(shù)。
3:當(dāng)程序調(diào)用延遲載入DLL中的函數(shù)時(shí),對該函數(shù)的調(diào)用會(huì)轉(zhuǎn)到_delayLoadHelper2函數(shù),來完成對延遲載入函數(shù)的解析。也就是說對延遲載入段中的函數(shù)的調(diào)用,實(shí)際上會(huì)調(diào)用_delayLoadHelper2函數(shù)。此函數(shù)會(huì)引用延遲載入段,然后調(diào)用LoadLibrary和GetProcAddress得到延遲載入函數(shù)的地址。一旦得到延遲載入函數(shù)的地址_delayLoadHelper2就會(huì)修復(fù)對該函數(shù)的調(diào)用(Windows核心編程的原話,至于如何修復(fù)不清楚。2011年12月8日注)。今后的調(diào)用將直接調(diào)用該延遲載入函數(shù)。注意:同一個(gè)DLL的其它函數(shù)仍然必須在第一次被調(diào)用的時(shí)候修復(fù)。對同一DLL中某一延遲函數(shù)的調(diào)用并不會(huì)對其他延遲函數(shù)的調(diào)用進(jìn)行修復(fù)
關(guān)于延遲載入函數(shù)暫時(shí)介紹這么多。感興趣的話可以參考其他文獻(xiàn)。
《參考自windows核心編程》第五版第四部分。以上僅僅是個(gè)人總結(jié),如有紕漏請不吝賜教!