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