Windows核心编程 第二十章 DLL的高级操作技术
第2?0章?D?L?L的高級操作技術(shù)
? ? 看了下這章的內(nèi)容,談不上高級,都是些常用相關(guān),但是還是有一些細(xì)節(jié)需要注意。
?
20.1?DLL模塊的顯式加載和符號鏈接
????如果線程需要調(diào)用D?L?L模塊中的函數(shù),那么D?L?L的文件映像必須映射到調(diào)用線程的進(jìn)程地址空間中。可以用兩種方法進(jìn)行這項操作。第一種方法是讓應(yīng)用程序的源代碼只引用?D?L?L中包含的符號。這樣,當(dāng)應(yīng)用程序啟動運行時,加載程序就能夠隱含加載(和鏈接)需要的?D?L?L。
第二種方法是在應(yīng)用程序運行時讓應(yīng)用程序顯式加載需要的?D?L?L并且顯式鏈接到需要的輸出符號。換句話說,當(dāng)應(yīng)用程序運行時,它里面的線程能夠決定它是否要調(diào)用?D?L?L中的函數(shù)。該線程可以將D?L?L顯式加載到進(jìn)程的地址空間,獲得D?L?L中包含的函數(shù)的虛擬內(nèi)存地址,然后使用該內(nèi)存地址調(diào)用該函數(shù)。這種方法的優(yōu)點是一切操作都是在應(yīng)用程序運行時進(jìn)行的。
20.1.1?顯式加載D?L?L模塊
無論何時,進(jìn)程中的線程都可以決定將一個?D?L?L映射到進(jìn)程的地址空間,方法是調(diào)用下面兩個函數(shù)中的一個:
HINSTANCE?LoadLibrary(PCTSTR?pszPathName)
HINSTANCE?LoadLibraryEx(
PCTSTR?pszDLLPathName,
HANDLE?hFile,
DWORD?dwFlags);
????這兩個函數(shù)均用于找出用戶系統(tǒng)上的文件映像(使用上一章中介紹的搜索算法),并設(shè)法將D?L?L的文件映像映射到調(diào)用進(jìn)程的地址空間中。兩個函數(shù)返回的?H?I?N?S?TA?N?C?E值用于標(biāo)識文件映像映射到的虛擬內(nèi)存地址。如果?D?L?L不能被映射到進(jìn)程的地址空間,則返回?N?U?L?L。若要了解關(guān)于錯誤的詳細(xì)信息,可以調(diào)用G?e?t?L?a?s?t?E?r?r?o?r.
你會注意到,L?o?a?d?L?i?b?r?a?r?y?E?x函數(shù)配有兩個輔助參數(shù),即h?F?i?l?e和d?w?F?l?a?g?s。參數(shù)h?F?i?l?e保留供將來使用,現(xiàn)在必須是?N?U?L?L。對于參數(shù)?d?w?F?l?a?g?s,必須將它設(shè)置為?0,或者設(shè)置為D?O?N?T?_?R?E?S?O?LV?E?_?D?L?L?_?R?E?F?E?R?E?N?C?E?S、L?O?A?D?_?L?I?B?R?A?RY?_?A?S?_?D?ATA?F?I?L?E和L?O?A?D?_?W?I?T?H?_A?LT?E?R?E?D?_?S?E?A?R?C?H?_?PAT?H等標(biāo)志的一個組合。
1.?DON?T_RESOLV?E?_?D?L?L?_?R?E?F?E?R?E?N?C?E?S
DON?T_RESOLV?E?_?D?L?L?_?R?E?F?E?R?E?N?C?E?S標(biāo)志用于告訴系統(tǒng)將D?L?L映射到調(diào)用進(jìn)程的地址空間中。通常情況下,當(dāng)D?L?L被映射到進(jìn)程的地址空間中時,系統(tǒng)要調(diào)用D?L?L中的一個特殊函數(shù),即?D?l?l?M?a?i?n(本章后面介紹)。該函數(shù)用于對?D?L?L進(jìn)行初始化。?DON?T_RESOLV?E?_D?L?L?_?R?E?F?E?R?E?N?C?E?S標(biāo)志使系統(tǒng)不必調(diào)用D?l?l?M?a?i?n函數(shù)就能映射文件映像。
此外,D?L?L能夠輸入另一個D?L?L中包含的函數(shù)。當(dāng)系統(tǒng)將一個D?L?L映射到進(jìn)程的地址空間中時,它也要查看該?D?L?L是否需要其他的?D?L?L,并且自動加載這些?D?L?L。當(dāng)?D?O?NT?_?R?E?S?O?LV?E?_?D?L?L?_?R?E?F?E?R?E?N?C?E?S標(biāo)志被設(shè)定時,系統(tǒng)并不自動將其他的D?L?L加載到進(jìn)程的地址空間中。
????2.?LOAD_LIBRARY?_?A?S?_?D?ATA?F?I?L?E
L?O?A?D?_?L?I?B?R?A?RY?_?A?S?_?D?ATA?F?I?L?E標(biāo)志與DON?T_RESOLV?E?_?D?L?L?_?R?E?F?E?R?E?N?C?E?S標(biāo)志相類似,因為系統(tǒng)只是將D?L?L映射到進(jìn)程的地址空間中,就像它是數(shù)據(jù)文件一樣。系統(tǒng)并不花費額外的時間來準(zhǔn)備執(zhí)行文件中的任何代碼。例如,當(dāng)一個?D?L?L被映射到進(jìn)程的地址空間中時,系統(tǒng)要查看D?L?L中的某些信息,以確定應(yīng)該將哪些頁面保護(hù)屬性賦予文件的不同的節(jié)。如果設(shè)定了L?O?A?D?_?L?I?B?R?A?RY?_?A?S?_?D?ATA?F?I?L?E標(biāo)志,系統(tǒng)將以它要執(zhí)行文件中的代碼時的同樣方式來設(shè)置頁面保護(hù)屬性。
由于下面幾個原因,該標(biāo)志是非常有用的。首先,如果有一個?D?L?L(它只包含資源,但不包含函數(shù)),那么可以設(shè)定這個標(biāo)志,使?D?L?L的文件映像能夠映射到進(jìn)程的地址空間中。然后可以在調(diào)用加載資源的函數(shù)時,使用?L?o?a?d?L?i?b?r?a?r?y?E?x函數(shù)返回的H?I?N?S?TA?N?C?E值。通常情況下,加載一個.?e?x?e文件,就能夠啟動一個新進(jìn)程,但是也可以使用?L?o?a?d?L?i?b?r?a?r?y?E?x函數(shù)將.?e?x?e文件的映像映射到進(jìn)程的地址空間中。借助映射的?.?e?x?e文件的H?I?N?S?TA?N?C?E值,就能夠訪問文件中的資源。由于.?e?x?e文件沒有D?l?l?M?a?i?n函數(shù),因此,當(dāng)調(diào)用L?o?a?d?L?i?b?r?a?r?y?E?x來加載一個.?e?x?e文件時,必須設(shè)定L?O?A?D?_?L?I?B?R?A?RY?_?A?S?_?D?ATA?F?I?L?E標(biāo)志。
3.?LOAD_WITH_ALT?E?R?E?D?_?S?E?A?R?C?H?_?PAT?H
L?O?A?D?_?W?I?T?H?_?A?LT?E?R?E?D?_?S?E?A?R?C?H?_?PAT?H標(biāo)志用于改變L?o?a?d?L?i?b?r?a?r?y?E?x用來查找特定的
D?L?L文件時使用的搜索算法。通常情況下,?L?o?a?d?L?i?b?r?a?r?y?E?x按照第1?9章講述的順序進(jìn)行文件的搜索。但是,如果設(shè)定了L?O?A?D?_?W?I?T?H?_?A?LT?E?R?E?D?_?S?E?A?R?C?H?_?PAT?H標(biāo)志,那么L?o?a?d?L?i?b?r?a?r?y?E?x函數(shù)就按照下面的順序來搜索文件:
1)?pszDLLPathName參數(shù)中設(shè)定的目錄。
2)?進(jìn)程的當(dāng)前目錄。
3)?Wi?n?d?o?w?s的系統(tǒng)目錄。
4)?Wi?n?d?o?w?s目錄。
5)?PAT?H環(huán)境變量中列出的目錄。
20.1.2?顯式卸載D?L?L模塊
當(dāng)進(jìn)程中的線程不再需要?D?L?L中的引用符號時,可以從進(jìn)程的地址空間中顯式卸載?D?L?L,方法是調(diào)用下面的函數(shù):
BOOL?FreeLibrary(HINSTANCE?hinstDLL);
????必須傳遞?H?I?N?S?TA?N?C?E?值,以便標(biāo)識要卸載的?D?L?L。該值是較早的時候調(diào)用
L?o?a?d?L?i?b?r?a?r?y?(?E?x?)而返回的值。
也可以通過調(diào)用下面的函數(shù)從進(jìn)程的地址空間中卸載D?L?L:
VOID?FreeLibraryAndExitThread(
????HINSTANCE?hinstDll,
????DWORD?dwExitCode);
該函數(shù)是在K?e?r?n?e?l?3?2?.?d?l?l中實現(xiàn)的,如下所示:
VOID?FreeLibraryAndExitThread(HINSTANCE?hinstDll,DWORD?dwExitCode){
????FreeLibrary(hinstDll);
????ExitThread(dwExitCode);
}
初看起來,這并不是個非常高明的代碼,你可能不明白,為什么?M?i?c?r?o?s?o?f?t要創(chuàng)建
F?r?e?e?L?i?b?r?a?r?y?A?n?d?E?x?i?t?T?h?r?e?a?d這個函數(shù)。其原因與下面的情況有關(guān):假定你要編寫一個?D?L?L,當(dāng)它被初次映射到進(jìn)程的地址空間中時,該D?L?L就創(chuàng)建一個線程。當(dāng)該線程完成它的操作時,它通過調(diào)用F?r?e?e?L?i?b?r?a?r?y函數(shù),從進(jìn)程的地址空間中卸載該?D?L?L,并且終止運行,然后立即調(diào)用E?x?i?t?T?h?r?e?a?d。
但是,如果線程分開調(diào)用F?r?e?e?L?i?b?r?a?r?y和E?x?i?t?T?h?r?e?a?d,就會出現(xiàn)一個嚴(yán)重的問題。這個問題是調(diào)用F?r?e?e?L?i?b?r?a?r?y會立即從進(jìn)程的地址空間中卸載D?L?L。當(dāng)調(diào)用的F?r?e?e?L?i?b?r?a?r?y返回時,包含對E?x?i?t?T?h?r?e?a?d調(diào)用的代碼就不再可以使用,因此線程將無法執(zhí)行任何代碼。這將導(dǎo)致訪問違規(guī),同時整個進(jìn)程終止運行。
但是,如果線程調(diào)用F?r?e?e?L?i?b?r?a?r?y?A?n?d?E?x?i?t?T?h?r?e?a?d,該函數(shù)調(diào)用F?r?e?e?L?i?b?r?a?r?y,使D?L?L立即被卸載。下一個執(zhí)行的指令是在K?e?r?n?e?l?3?2?.?d?l?l中,而不是在剛剛被卸載的D?L?L中。這意味著該線程能夠繼續(xù)執(zhí)行,并且可以調(diào)用E?x?i?t?T?h?r?e?a?d。E?x?i?t?T?h?r?e?a?d使該線程終止運行并且不返回。
調(diào)用G?e?t?M?o?d?u?l?e?H?a?n?d?l?e函數(shù),線程就能夠確定D?L?L是否已經(jīng)被映射到進(jìn)程的地址空間中:
HINSTANCE?hinstDll?=??GetModuleHandle(“MyLib”);
If(hinstDll??==?NULL){
????hinstDll??=?LoadLibrary(“MyLib”);
}
如果只有?D?L?L的H?I?N?S?TA?N?C?E值,那么可以調(diào)用?G?e?t?M?o?d?u?l?e?F?i?l?e?N?a?m?e函數(shù),確定?D?L?L(或.?e?x?e)的全路徑名:
DWORD?GetModuleFileName(
????HINSTANCE?hinstModule,
????PTSTR?pszPathName,
????DWORD?cchPath);
第一個參數(shù)是D?L?L(或.?e?x?e)的H?I?N?S?TA?N?C?E。第二個參數(shù)p?s?z?P?a?t?h?N?a?m?e是該函數(shù)將文件映像的全路徑名放入的緩存的地址。第三參數(shù)c?c?h?P?a?t?h用于設(shè)定緩存的大小(以字符為計量單位)。
20.1.3?顯式鏈接到一個輸出符號
根據(jù)函數(shù)名字加載:
FARPOC?pfn?=?GetProcAddress(hinstDll?,”SomeFuncInDll”);
根據(jù)函數(shù)編號加載:
FARPOC?pfn?=?GetProcAddress(hinstDll?,MAKEINTRESOURCE(2));
????這種用法假設(shè)你知道你需要的符號名被?D?L?L創(chuàng)建程序賦予了序號值2。同樣,我要再次強(qiáng)調(diào),M?i?c?r?o?s?o?f?t非常反對使用序號,因此你不會經(jīng)常看到G?e?t?P?r?o?c?A?d?d?r?e?s?s的這個用法。
????這兩種方法都能夠提供包含在D?L?L中的必要符號的地址。如果D?L?L模塊的輸出節(jié)中不存在你需要的符號,G?e?t?P?r?o?c?A?d?d?r?e?s?s就返回N?U?L?L,表示運行失敗。
應(yīng)該知道,調(diào)用G?e?t?P?r?o?c?A?d?d?r?e?s?s的第一種方法比第二種方法要慢,因為系統(tǒng)必須進(jìn)行字符串的比較,并且要搜索傳遞的符號名字符串。對于第二種方法來說,如果傳遞的序號尚未被分配給任何輸出的函數(shù),那么?G?e?t?P?r?o?c?A?d?d?r?e?s?s就會返回一個非N?U?L?L值。這個返回值將會使你的應(yīng)用程序錯誤地認(rèn)為你已經(jīng)擁有一個有效的地址,而實際上你并不擁有這樣的地址。如果試圖調(diào)用該地址,肯定會導(dǎo)致線程引發(fā)一個訪問違規(guī)。我在早期從事?Wi?n?d?o?w?s編程時,并不完全理解這個行為特性,因此多次出現(xiàn)這樣的錯誤。所以一定要小心(這個行為特性是應(yīng)該避免使用序號而使用符號名的另一個原因)。
20.2?DLL的進(jìn)入點函數(shù)
????參數(shù)h?i?n?s?t?D?l?l包含了D?L?L的實例句柄。與(?w?)?Wi?n?M?a?i?n函數(shù)的h?i?n?s?t?E?x?e參數(shù)一樣,這個值用于標(biāo)識D?L?L的文件映像被映射到進(jìn)程的地址空間中的虛擬內(nèi)存地址。通常應(yīng)將這個參數(shù)保存在一個全局變量中,這樣就可以在調(diào)用加載資源的函數(shù)(如?D?i?a?l?o?g?B?o?x和L?o?a?d?S?t?r?i?n?g)時使用它。最后一個參數(shù)是f?I?m?p?L?o?a?d,如果D?L?L是隱含加載的,那么該參數(shù)將是個非?0值,如果D?L?L是顯式加載的,那么它的值是0。
????參數(shù)f?d?w?R?e?a?s?o?n用于指明系統(tǒng)為什么調(diào)用該函數(shù)。該參數(shù)可以使用?4個值中的一個。這4個值是:D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H、D?L?L?_?P?R?O?C?E?S?S?_?D?E?TA?C?H、D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H或
D?L?L?_?T?H?R?E?A?D?_?D?E?TA?C?H。這些值將在下面介紹。
????注意?必須記住,D?L?L使用D?l?l?M?a?i?n函數(shù)來對它們進(jìn)行初始化。當(dāng)你的D?l?l?M?a?i?n函數(shù)執(zhí)行時,同一個地址空間中的其他D?L?L可能尚未執(zhí)行它們的D?l?l?M?a?i?n函數(shù)。這意味著它們尚未初始化,因此你應(yīng)該避免調(diào)用從其他D?L?L中輸入的函數(shù)。此外,你應(yīng)該避免從D?l?l?M?a?i?n內(nèi)部調(diào)用L?o?a?d?L?i?b?r?a?r?y?(?E?x?)和F?r?e?e?L?i?b?r?a?r?y函數(shù),因為這些函數(shù)會形式一個依賴性循環(huán)。
????Platform?SDK文檔說,你的D?l?l?M?a?i?n函數(shù)只應(yīng)該進(jìn)行一些簡單的初始化,比如設(shè)置
本地存儲器(第?2?1章介紹),創(chuàng)建內(nèi)核對象和打開文件等。你還必須避免調(diào)用?U?s?e?r、S?h?e?l?l、O?D?B?C、C?O?M、R?P?C和套接字函數(shù)(即調(diào)用這些函數(shù)的函數(shù)),因為它們的D?L?L也許尚未初始化?,或者這些函數(shù)可能在內(nèi)部調(diào)用L?o?a?d?L?i?b?r?a?r?y?(?E?x?)函數(shù),這同樣會形成一個依賴性循環(huán)。
????另外,如果創(chuàng)建全局性的或靜態(tài)的C?+?+對象,那么應(yīng)該注意可能存在同樣的問題,因為在你調(diào)用D?l?l?M?a?i?n函數(shù)的同時,這些對象的構(gòu)造函數(shù)和析構(gòu)函數(shù)也會被調(diào)用。
20.2.1?DLL_PROCESS_AT?TA?C?H通知
????當(dāng)D?L?L被初次映射到進(jìn)程的地址空間中時,系統(tǒng)將調(diào)用該?D?L?L的D?l?l?M?a?i?n函數(shù),給它傳遞參數(shù)f?d?w?R?e?a?s?o?n的值D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H。只有當(dāng)D?L?L的文件映像初次被映射時,才會出現(xiàn)這種情況。如果線程在后來為已經(jīng)映射到進(jìn)程的地址空間中的D?L?L調(diào)用L?o?a?d?L?i?b?r?a?r?y?(?E?x?)函數(shù),那么操作系統(tǒng)只是遞增?D?L?L的使用計數(shù),它并不再次用?D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H的值來調(diào)用D?L?L的D?l?l?M?a?i?n函數(shù)。
當(dāng)處理D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H時,D?L?L應(yīng)該執(zhí)行D?L?L中的函數(shù)要求的任何與進(jìn)程相關(guān)的初始化。例如,D?L?L可能包含需要使用它們自己的堆棧(在進(jìn)程的地址空間中創(chuàng)建)的函數(shù)。通過在處理D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H通知時調(diào)用H?e?a?p?C?r?e?a?t?e函數(shù),該D?L?L的D?l?l?M?a?i?n函數(shù)就能夠創(chuàng)建這個堆棧。已經(jīng)創(chuàng)建的堆棧的句柄可以保存在D?L?L函數(shù)有權(quán)訪問的一個全局變量中。
當(dāng)D?l?l?M?a?i?n處理一個D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H通知時,D?l?l?M?a?i?n的返回值能夠指明D?L?L的初始化是否已經(jīng)取得成功。如果對H?e?a?p?C?r?e?a?t?e的調(diào)用取得了成功,D?l?l?M?a?i?n應(yīng)該返回T?R?U?E。如果堆棧不能創(chuàng)建,它應(yīng)該返回?FA?L?S?E。如果?f?d?w?R?e?a?s?o?n使用的是其他的值,即?D?L?L?_P?R?O?C?E?S?S?_?D?E?TA?C?H、D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H和D?L?L?_?T?H?R?E?A?D?_?D?E?TA?C?H,那么系統(tǒng)將忽略D?l?l?M?a?i?n返回的值。
20.2.2?DLL_PROCESS_DETA?C?H通知
D?L?L從進(jìn)程的地址空間中被卸載時,系統(tǒng)將調(diào)用?D?L?L的D?l?l?M?a?i?n函數(shù),給它傳遞f?d?w?R?e?a?s?o?n的值D?L?L?_?P?R?O?C?E?S?S?_?D?E?TA?C?H。當(dāng)D?L?L處理這個值時,它應(yīng)該執(zhí)行任何與進(jìn)程相關(guān)的清除操作。
注意,D?L?L能夠阻止進(jìn)程終止運行。例如,當(dāng)?D?l?l?M?a?i?n接收到D?L?L?_?P?R?O?C?E?S?S?_?D?E?TA?C?H通知時,它就會進(jìn)入一個無限循環(huán)。只有當(dāng)每個?D?L?L都已完成對D?L?L?_?P?R?O?C?E?S?S?_?D?E?TA?C?H通知的處理時,操作系統(tǒng)才會終止該進(jìn)程的運行。
注意?如果因為系統(tǒng)中的某個線程調(diào)用了Te?r?m?i?n?a?t?e?P?r?o?c?e?s?s而使進(jìn)程終止運行,那么系統(tǒng)將不調(diào)用帶有D?L?L?_?P?R?O?C?E?S?S?_?D?E?TA?C?H值的?D?L?L的D?l?l?M?a?i?n函數(shù)。這意味著映射到進(jìn)程的地址空間中的任何D?L?L都沒有機(jī)會在進(jìn)程終止運行之前執(zhí)行任何清除操作。這可能導(dǎo)致數(shù)據(jù)的丟失。只有在迫不得已的情況下,才能使用?Te?r?m?i?n?a?t?e?P?r?o?c?e?s?s函數(shù)。
20.2.3?DLL_THREAD_AT?TA?C?H通知
????當(dāng)在一個進(jìn)程中創(chuàng)建線程時,系統(tǒng)要查看當(dāng)前映射到該進(jìn)程的地址空間中的所有?D?L?L文件映像,并調(diào)用每個文件映像的帶有?D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的D?l?l?M?a?i?n函數(shù)。這可以告訴所有的D?L?L執(zhí)行每個線程的初始化操作。新創(chuàng)建的線程負(fù)責(zé)執(zhí)行?D?L?L的所有D?l?l?M?a?i?n函數(shù)中的代碼。只有當(dāng)所有的D?L?L都有機(jī)會處理該通知時,系統(tǒng)才允許新線程開始執(zhí)行它的線程函數(shù)。
當(dāng)一個新D?L?L被映射到進(jìn)程的地址空間中時,如果該進(jìn)程內(nèi)已經(jīng)有若干個線程正在運行,那么系統(tǒng)將不為現(xiàn)有的線程調(diào)用帶有D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的DDL?的D?l?l?M?a?i?n函數(shù)。只有當(dāng)新線程創(chuàng)建時D?L?L被映射到進(jìn)程的地址空間中,它才調(diào)用帶有D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的D?L?L的D?l?l?M?a?i?n函數(shù)。另外要注意,系統(tǒng)并不為進(jìn)程的主線程調(diào)用帶有?D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的任何D?l?l?M?a?i?n函數(shù)。進(jìn)程初次啟動時映射到進(jìn)程的地址空間中的任何?D?L?L均接收?D?L?L?_P?R?O?C?E?S?S?_?AT?TA?C?H通知,而不是D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H通知。
20.2.4?DLL_THREAD_DETA?C?H通知
????讓線程終止運行的首選方法是使它的線程函數(shù)返回。這使得系統(tǒng)可以調(diào)用?E?x?i?t?T?h?r?e?a?d來撤消該線程。E?x?i?t?T?h?r?e?a?d函數(shù)告訴系統(tǒng),該線程想要終止運行,但是系統(tǒng)并不立即將它撤消。相反?,?它?要?取?出?這?個?即?將?被?撤?消?的?線?程?,?并?讓?它?調(diào)?用?已?經(jīng)?映?射?的?D?L?L?的?所?有?帶?有D?L?L?_?T?H?R?E?A?D?_?D?E?TACH?值的D?l?l?M?a?i?n函數(shù)。這個通知告訴所有的D?L?L執(zhí)行每個線程的清除操作。例如,D?L?L版本的C?/?C?+?+運行期庫能夠釋放它用于管理多線程應(yīng)用程序的數(shù)據(jù)塊。
????注意,?D?L?L能夠防止線程終止運行。例如,當(dāng)?D?l?l?M?a?i?n函數(shù)接收到?D?L?L?_?T?H?R?E?A?D?_D?E?TA?C?H通知時,它就能夠進(jìn)入一個無限循環(huán)。只有當(dāng)每個?D?L?L已經(jīng)完成對D?L?L?_?T?H?R?E?A?D?_D?E?TA?C?H通知的處理時,操作系統(tǒng)才會終止線程的運行。
注意?如果因為系統(tǒng)中的線程調(diào)用Te?r?m?i?n?a?t?e?T?h?r?e?a?d函數(shù)而使該線程終止運行,那么系統(tǒng)將不調(diào)用帶有D?L?L?_?T?H?R?E?A?D?_?D?E?TA?C?H值的D?L?L的所有D?l?l?M?a?i?n函數(shù)。這意味著映射到進(jìn)程的地址空間中的任何一個D?L?L都沒有機(jī)會在線程終止運行之前執(zhí)行任何清除操作。這可能導(dǎo)致數(shù)據(jù)的丟失。與Te?r?m?i?n?a?t?e?P?r?o?c?e?s?s一樣,只有在迫不得已的時候,才可以使用Te?r?m?i?n?a?t?e?T?h?r?e?a?d函數(shù)。
如果當(dāng)D?L?L被撤消時仍然有線程在運行,那么就不為任何線程調(diào)用帶有?D?L?L?_?T?H?R?E?A?D?_D?E?TA?C?H值的D?l?l?M?a?i?n。可以在進(jìn)行D?L?L?_?T?H?R?E?A?D?_?D?E?TA?C?H的處理時查看這個情況,這樣就能夠執(zhí)行必要的清除操作。
上述規(guī)則可能導(dǎo)致發(fā)生下面這種情況。當(dāng)進(jìn)程中的一個線程調(diào)用L?o?a?d?L?i?b?r?a?r?y來加載D?L?L時,系統(tǒng)就會調(diào)用帶有D?L?L?_?P?R?O?C?E?S?S?_?AT?TA?C?H值的D?L?L的D?l?l?M?a?i?n函數(shù)(注意,沒有為該線程發(fā)送D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H通知)。接著,負(fù)責(zé)加載D?L?L的線程退出,從而導(dǎo)致D?L?L的D?l?l?M?a?i?n函數(shù)被再次調(diào)用,這次調(diào)用時帶有D?L?L?_?T?H?R?E?A?D?_?D?E?TA?C?H值。注意,D?L?L得到通知說,該線程將被撤消,盡管它從未收到D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H的這個通知,這個通知告訴該庫說線程已經(jīng)附加。由于這個原因,當(dāng)執(zhí)行任何特定的線程清除操作時,必須非常小心。不過大多數(shù)程序在編寫時就規(guī)定調(diào)用L?o?a?d?L?i?b?r?a?r?y的線程與調(diào)用F?r?e?e?L?i?b?r?a?r?y的線程是同一個線程。
20.2.5?順序調(diào)用D?l?l?M?a?i?n
系統(tǒng)是順序調(diào)用D?L?L的D?l?l?M?a?i?n函數(shù)的。為了理解這樣做的意義,可以考慮下面這樣一個環(huán)境。假設(shè)一個進(jìn)程有兩個線程,線程?A和線程B。該進(jìn)程還有一個D?L?L,稱為S?o?m?e?D?L?L?.?d?l?l,它被映射到了它的地址空間中。兩個線程都準(zhǔn)備調(diào)用?C?r?e?a?t?e?T?h?r?e?a?d函數(shù),以便再創(chuàng)建兩個線程,即線程C和線程D。當(dāng)線程A調(diào)用C?r?e?a?t?e?T?h?r?e?a?d來創(chuàng)建線程C時,系統(tǒng)調(diào)用帶有?D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的S?o?m?e?D?L?L?.?d?l?l的D?l?l?M?a?i?n函數(shù)。
當(dāng)線程C執(zhí)行D?l?l?M?a?i?n函數(shù)中的代碼時,線程B調(diào)用C?r?e?a?t?e?T?h?r?e?a?d函數(shù)來創(chuàng)建線程D。這時系統(tǒng)必須再次調(diào)用帶有D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H值的D?l?l?M?a?i?n函數(shù),這次是讓線程D?執(zhí)行代碼。但是,系統(tǒng)是順序調(diào)用?D?l?l?M?a?i?n函數(shù)的,因此系統(tǒng)會暫停線程?D的運行,直到線程C完成對D?l?l?M?a?i?n函數(shù)中的代碼的處理并且返回為止。
當(dāng)線程C完成D?l?l?M?a?i?n的處理后,它就開始執(zhí)行它的線程函數(shù)。這時系統(tǒng)喚醒線程?D,讓它處理D?l?l?M?a?i?n中的代碼。當(dāng)它返回時,線程D開始處理它的線程函數(shù)。
看下面的常見錯誤(DllMain里創(chuàng)建線程導(dǎo)致死鎖)
自己測試了一次有WaitForSingleObject會死鎖,沒有就不會死鎖了。
?
當(dāng)C?r?e?a?t?e?T?h?r?e?a?d函數(shù)被調(diào)用時,系統(tǒng)首先創(chuàng)建線程的內(nèi)核對象和線程的堆棧。然后它在內(nèi)部調(diào)用Wa?i?t?F?o?r?S?i?n?g?l?e?O?b?j?e?c?t函數(shù),傳遞進(jìn)程的互斥對象的句柄。一旦新線程擁有該互斥對象,系統(tǒng)就讓新線程用D?L?L?_?T?H?R?E?A?D?_?AT?TA?C?H的值調(diào)用每個D?L?L的D?l?l?M?a?i?n函數(shù)。只有在這個時候,系統(tǒng)才調(diào)用R?e?l?e?a?s?e?M?u?t?e?x,釋放對進(jìn)程的互斥對象的所有權(quán)。由于系統(tǒng)采用這種方式來運行,因此添加對D?i?s?a?b?l?e?T?h?r?e?a?d?L?i?b?r?a?r?y?C?a?l?l?s的調(diào)用,并不會防止線程被暫停運行。防止線程被暫停運行的唯一辦法是重新設(shè)計這部分源代碼,使得?Wa?i?t?F?o?r?S?i?n?g?l?e?O?b?j?e?c?t不會在任何?D?L?L的D?l?l?M?a?i?n函數(shù)中被調(diào)用。
20.3?延遲加載D?L?L
????Microsoft?Visual?C++?6.0提供了一個出色的新特性,它能夠使?D?L?L的操作變得更加容
易。這個特性稱為延遲加載?D?L?L。延遲加載的?D?L?L是個隱含鏈接的?D?L?L,它實際上要等到你的代碼試圖引用?D?L?L中包含的一個符號時才進(jìn)行加載。延遲加載的?D?L?L在下列情況下是非常有用的:
??如果你的應(yīng)用程序使用若干個?D?L?L,那么它的初始化時間就比較長,因為加載程序要將
所有需要的D?L?L映射到進(jìn)程的地址空間中。解決這個問題的方法之一是在進(jìn)程運行的時候分開加載各個D?L?L。延遲加載的D?L?L能夠更容易地完成這樣的加載。
??如果調(diào)用代碼中的一個新函數(shù),然后試圖在老版本的系統(tǒng)上運行你的應(yīng)用程序,而該系
統(tǒng)中沒有該函數(shù),那么加載程序就會報告一個錯誤,并且不允許該應(yīng)用程序運行。你需要一種方法讓你的應(yīng)用程序運行,然后,如果(在運行時)發(fā)現(xiàn)該應(yīng)用程序在老的系統(tǒng)上運行,那么你將不調(diào)用遺漏的函數(shù)。例如,一個應(yīng)用程序在?Windows?2000上運行時想要使用P?S?A?P?I函數(shù),而在Windows?98上運行想要使用To?o?l?H?e?l?p函數(shù)(比如P?r?o?c?e?s?s?3?2?N?e?x?t)。
當(dāng)該應(yīng)用程序初始化時,它調(diào)用?G?e?t?Ve?r?s?i?o?n?E?x函數(shù)來確定主操作系統(tǒng),并正確地調(diào)用相應(yīng)的其他函數(shù)。如果試圖在Windows?98上運行該應(yīng)用程序,就會導(dǎo)致加載程序顯示一條
錯誤消息,因為Windows?98上并不存在P?S?A?P?I?.?d?l?l模塊。同樣,延遲加載的D?L?L能夠使你非常容易地解決這個問題。
20.4?函數(shù)轉(zhuǎn)發(fā)器
???函數(shù)轉(zhuǎn)發(fā)器是D?L?L的輸出節(jié)中的一個項目,用于將對一個函數(shù)的調(diào)用轉(zhuǎn)至另一個?D?L?L中的另一個函數(shù)。例如,如果在Windows?2000?的K?e?r?n?e?l?3?2?.?d?l?l上運行Visual?C++的D?u?m?p?B?i?n實用程序,那么將看到類似下面的一部分輸出:
?
????這個輸出顯示了4個轉(zhuǎn)發(fā)函數(shù)。每當(dāng)你的應(yīng)用程序調(diào)用H?e?a?p?A?l?l?o?c、H?e?a?p?F?r?e?e、H?e?a?p?R?e?A?l?l?o?c或H?e?a?p?S?i?z?e時,你的可執(zhí)行模塊就會自動與K?e?r?n?e?l?3?2?.?d?l?l相鏈接。當(dāng)激活你的可執(zhí)行模塊時,加載程序就加載?K?e?r?n?e?l?3?2?.?d?l?l并看到轉(zhuǎn)發(fā)的函數(shù)實際上包含在?N?T?D?L?L?.?d?l?l中。然后它也加載N?T?D?L?L?.?d?l?l。當(dāng)你的可執(zhí)行模塊調(diào)用?H?e?a?p?A?l?l?o?c時,它實際上調(diào)用的是?N?T?D?L?L?.?d?l?l中的R?t?l?A?l?l?o?c?a?t?e?H?e?a?p函數(shù)。系統(tǒng)中的任何地方都不存在H?e?a?p?A?l?l?o?c函數(shù)。
如果調(diào)用下面的函數(shù),G?e?t?P?r?o?c?A?d?d?r?e?s?s就會查看K?e?r?n?e?l?3?2的輸出節(jié),發(fā)現(xiàn)H?e?a?p?A?l?l?o?c是個轉(zhuǎn)發(fā)函數(shù),然后按遞歸方式調(diào)用?G?e?t?P?r?o?c?A?d?d?r?e?s?s函數(shù),查找?N?T?D?L?L?.?d?l?l的輸出節(jié)中的?R?t?l?A?l?-l?o?c?a?t?e?H?e?a?p。
?
????這個p?r?a?g?m?a告訴鏈接程序,被編譯的?D?L?L應(yīng)該輸出一個名叫?S?o?m?e?F?u?n?c的函數(shù)。但是S?o?m?e?F?u?n?c函數(shù)的實現(xiàn)實際上位于另一個名叫?S?o?m?e?O?t?h?e?r?F?u?n?c的函數(shù)中,該函數(shù)包含在稱為D?l?l?Wo?r?k?.?d?l?l的模塊中。必須為你想要轉(zhuǎn)發(fā)的每個函數(shù)創(chuàng)建一個單獨的?p?r?a?g?m?a代碼行。
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Windows核心编程 第二十章 DLL的高级操作技术的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第十九章 DLL
- 下一篇: Windows PE导出表编程2(重组导