延遲加載DLL

      MicrosoftVisualC++6.0提供了一個出色的新特性,它能夠使DLL的操作變得更加容易。這個特性稱爲延遲加載DLL。延遲加載的DLL是個隱含鏈接的DLL,它實際上要等到你的代碼試圖引用DLL中包含的一個符號時才進行加載。延遲加載的DLL在下列情況下是非常有用的:>>如果你的應用程序使用若干個DLL,那麼它的初始化時間就比較長,因爲加載程序要將所有需要的DLL映射到進程的地址空間中。解決這個問題的方法之一是在進程運行的時候分開加載各個DLL。延遲加載的DLL能夠更容易地完成這樣的加載。>>如果調用代碼中的一個新函數,然後試圖在老版本的系統上運行你的應用程序,而該系統中沒有該函數,那麼加載程序就會報告一個錯誤,並且不允許該應用程序運行。你需要一種方法讓你的應用程序運行,然後,如果(在運行時)發現該應用程序在老的系統上運行,那麼你將不調用遺漏的函數。例如,一個應用程序在Windows2000上運行時想要使用PSAPI函數,而在Windows98上運行想要使用ToolHelp函數(比如Process32Next)。當該應用程序初始化時,它調用GetVersionEx函數來確定主操作系統,並正確地調用相應的其他函數。如果試圖在Windows98上運行該應用程序,就會導致加載程序顯示一條錯誤消息,因爲Windows98上並不存在PSAPI.dll模塊。同樣,延遲加載的DLL能夠使你非常容易地解決這個問題。我花費了相當多的時間來檢驗VisualC++6.0中的延遲加載DLL特性,必須承認,Microsoft在實現這個特性方面做了非常出色的工作。它提供了許多特性,並且在Windows98和Windows2000上運行得都很好。下面讓我們從比較容易的操作開始介紹,也就是使延遲加載DLL能夠運行。首先,你象平常那樣創建一個DLL。也要象平常那樣創建一個可執行模塊,但是必須修改兩個鏈接程序開關,並且重新鏈接可執行模塊。下面是需要添加的兩個鏈接程序開關:/Lib:DelayImp.lib/DelayLoad:MyDll.dllLib開關告訴鏈接程序將一個特殊的函數--delayLoadHelper嵌入你的可執行模塊。第二個開關將下列事情告訴鏈接程序:>>從可執行模塊的輸入節中刪除MyDll.dll,這樣,當進程被初始化時,操作系統的加載程序就不會顯式加載DLL。>>將新的DelayImport(延遲輸入)節(稱爲.didata)嵌入可執行模塊,以指明哪些函數正在從MyDll.dll輸入。>>通過轉移到對--delayLoadHelper函數的調用,轉換到對延遲加載函數的調用。當應用程序運行時,對延遲加載函數的調用實際上是對--delayLoadHelper函數的調用。該函數引用特殊的DelayImport節,並且知道調用LoadLibrary之後再調用GetProcAddress。一旦獲得延遲加載函數的地址,--delayLoadHelper就要安排好對該函數的調用,這樣,將來的調用就會直接轉向對延遲加載函數的調用。注意,當第一次調用同一個DLL中的其他函數時,必須對它們做好安排。另外,可以多次設定/delayLoad鏈接程序的開關,爲想要延遲加載的每個DLL設定一次開關。好了,整個操作過程就這麼簡單。但是還應該考慮另外兩個問題。通常情況下,當操作系統的加載程序加載可執行模塊時,它將設法加載必要的DLL。如果一個DLL無法加載,那麼加載程序就會顯示一條錯誤消息。如果是延遲加載的DLL,那麼在進行初始化時將不檢查是否存在DLL。如果調用延遲加載函數時無法找到該DLL,--delayLoadHelper函數就會引發一個軟件異常條件。可以使用結構化異常處理(SEH)方法來跟蹤該異常條件。如果不跟蹤該異常條件,那麼你的進程就會終止運行(SEH將在第23、24和25章中介紹)。當--delayLoadHelper確實找到你的DLL,但是要調用的函數不在該DLL中時,將會出現另一個問題。比如,如果加載程序找到一個老的DLL版本,就會發生這種情況。在這種情況下,--delayLoadHelper也會引發一個軟件異常條件,對這個軟件異常條件的處理方法與上面相同。下一節介紹的示例應用程序顯示瞭如何正確地編寫SEH代碼以便處理這些錯誤。你會發現代碼中有許多其他元素,這些元素與SEH和錯誤處理毫無關係。但是這些元素與你使用延遲加載的DLL時可以使用的輔助特性有關。下面將要介紹這些特性。如果你不使用更多的高級特性,可以刪除這些額外的代碼。如你所見,VisualC++開發小組定義了兩個軟件異常條件代碼,即VcppException(ERROR_SEVERITY_ERROR、ERROR_MOD_NOT_FOUND)和VcppException(ERROR_SEVERITY_ERROR、ERROR_PROC_NOT_FOUND)。這些代碼分別用於指明DLL模塊沒有找到和函數沒有找到。我的異常過濾函數DelayLoadDllExceptionFilter用於查找這兩個異常代碼。如果兩個代碼都沒有找到,過濾函數將返回EXCEPTION_CONTINUE_SEARCH,這與任何出色的過濾函數返回的值是一樣的(對於你不知道如何處理的異常代碼,請不要隨意刪除)。但是如果這兩個代碼中的一個已經找到,那麼--delayLoadHelper函數將提供一個指向包含某些輔助信息的DelayLoadInfo結構的指針。在VisualC++的DelayImp.h文件中,DelayLoadInfo結構定義爲下面的形式:typedef struct DelayLoadInfo{ DWORD cb;//sizeofstruct PCImgDelayDescr p1dd;//Rawdata(everythingisthere) FARPROC* ppfn;// Pointstoaddressoffunctiontoload LPCSTR szDll;//Nameofdll DelayLoadProc dlp;//Nameorordinalofprodure HMODULE hmodCur;//hInstanceofloadedlibrary FARPROC pfnCur;//Actualfunctionthatwillbecalled DWORD dwLastError;//Errorreceived}DelayLoadInfo,*PDelayLoadInfo;這個數據結構是由--delayLoadHelper函數來分配和初始化的。在該函數按步驟動態加載DLL並且獲得被調用函數的地址的過程中,它將填寫該結構的各個成員。在SEH結構的內部,成員szDll指向你要加載的DLL的名字,想要查看的函數則在成員dlp中。由於可以按序號或名字來查看各個函數,因此dlp成員類似下面的樣子:typedef struct DelayLoadProc{ BOOL fImportByName; union{ LPCSTRszProcName; DWORDdwOrdinal; }}DelayLoadProc;如果DLL已經加載成功,但是它不包含必要的函數,也可以查看成員hmodCur,以瞭解DLL被加載到的內存地址。也可以查看成員dwLastError,以瞭解是什麼錯誤導致了異常條件的引發。不過對於異常過濾函數來說,這是不必要的,因爲異常代碼能夠告訴你究竟發生了什麼問題。成員pfnCur包含了需要的函數的地址。在過濾函數中它總是置爲NULL,因爲--delayLoadHelper無法找到該函數的地址。在其餘的成員中,cb用於確定版本,pidd指向嵌入模塊中包含延遲加載的DLL和函數的節,ppfn是函數找到時,函數的地址應該放入的地址。最後兩個成員供--delayLoadHelper函數內部使用。它們有着超高級的用途,現在還沒有必要觀察或者瞭解這兩個成員。到現在爲止,已經講述瞭如何使用延遲加載的DLL和正確解決錯誤條件的基本方法。但是Microsoft的延遲加載DLL的實現代碼超出了迄今爲止我已講述的內容範圍。比如,你的應用程序能夠卸載延遲加載的DLL。假如你的應用程序需要一個特殊的DLL來打印一個文檔,那麼這個DLL就非常適合作爲一個延遲加載的DLL,因爲大部分時間它是不用的。不過,如果用戶選擇了Print命令,你就可以調用該DLL中的一個函數,然後它就能夠自動進行DLL的加載。這確實很好,但是,當文檔打印後,用戶可能不會立即打印另一個文檔,因此可以卸載這個DLL,釋放系統的資源。如果用戶決定打印另一個文檔,那麼DLL就可以根據用戶的要求再次加載。若要卸載延遲加載的DLL,必須執行兩項操作。首先,當創建可執行文件時,必須設定另一個鏈接程序開關(/delay:unload)。其次,必須修改源代碼,並且在你想要卸載DLL時調用--FUnloadDelayLoadedDLL函數:BOOL _FUnloadDelayLoadedDll(PCSTRszDll);/Delay:unload鏈接程序開關告訴鏈接程序將另一個節放入文件中。該節包含了你清除已經調用的函數時需要的信息,這樣它們就可以再次調用--delayLoadHelper函數。當調用--FUnloadDelayLoadedDll時,你將想要卸載的延遲加載的DLL的名字傳遞給它。該函數進入文件中的未卸載節,並清除DLL的所有函數地址,然後--FUnloadDelayLoadedDll調用FreeLibrary,以便卸載該DLL。下面要指出一些重要的問題。首先,千萬不要自己調用FreeLibrary來卸載DLL,否則函數的地址將不會被清除,這樣,當下次試圖調用DLL中的函數時,就會導致訪問違規。第二,當調用--FUnloadDelayLoadedDll時,傳遞的DLL名字不應該包含路徑,名字中的字母必須與你將DLL名字傳遞給/DelayLoad鏈接程序開關時使用的字母大小寫相同,否則,--FUnloadDelayLoadedDll的調用將會失敗。第三,如果永遠不打算卸載延遲加載的DLL,那麼請不要設定/Delay:unload鏈接程序開關,並且你的可執行文件的長度應該比較小。最後,如果你不從用/Delay:unload開關創建的模塊中調用--FUnloadDelayLoadedDll,那麼什麼也不會發生,--FUnloadDelayLoadedDll什麼操作也不執行,它將返回FALSE。延遲加載的DLL具備的另一個特性是,按照默認設置,調用的函數可以與一些內存地址相鏈接,在這些內存地址上,系統認爲函數將位於一個進程的地址中(本章後面將介紹鏈接的問題)。由於創建可鏈接的延遲加載的DLL節會使你的可執行文件變得比較大,因此鏈接程序也支持一個/Delay:nobind開關。因爲人們通常都喜歡進行鏈接,因此大多數應用程序不應該使用這個鏈接開關。延遲加載的DLL的最後一個特性是供高級用戶使用的,它真正顯示了Microsoft的注意力之所在。當--delayLoadHelper函數執行時,它可以調用你提供的掛鉤函數。這些函數將接收--delayLoadHelper函數的進度通知和錯誤通知。此外,這些函數可以重載DLL如何加載的方法以及如何獲取函數的虛擬內存地址的方法。若要獲得通知或重載的行爲特性,必須對你的源代碼做兩件事情。首先必須編寫類似清單20-1所示的DliHook函數那樣的掛鉤函數。DliHook框架函數並不影響--delayLoadHelper函數的運行。若要改變它的行爲特性,可啓動DliHook函數,然後根據需要對它進行修改。接着將函數的地址告訴--delayLoadHelper。在DelayImp.lib靜態鏈接庫中,定義了兩個全局變量,即--pfnDliNotifyHook和--pfnDliFailureHook。這兩個變量均屬於pfnDliHook類型:typedef FARPROC( WINAPI* PfnDllHook)( unsigneddllNotify, PDelayLoadInfopdl1);如你所見,這是個數據類型的函數,與我的DliHook函數的原型相匹配。在DelayImp.lib文件中,兩個變量被初始化爲NULL,它告訴--delayLoadHelper不要調用任何掛鉤函數。若要使你的函數被調用,必須將這兩個函數中的一個設置爲掛鉤函數的地址。在我的代碼中,我只是將下面兩行代碼添加到全局作用域:PfnDllHook_pfnDllNotifyHook=DllHook;PfnDllHook_pfnFailureHook=DllHook;如你所見,--delayLoadHelper實際上是與兩個回調函數一道運行的。它調用一個函數以便報告通知,調用另一個函數來報告失敗情況。由於這兩個函數的原型是相同的,而第一個參數dliNotify告訴爲什麼調用這個函數,因此我總是通過創建單個函數並將兩個變量設置爲指向我的一個函數,使我的工作變得簡單一些。VisualC++6.0的延遲加載DLL的新特性非常出色,許多編程人員幾年前就希望使用這個特性。可以想像許多應用程序(尤其是Microsoft的應用程序)都將充分利用這個特性
發佈了15 篇原創文章 · 獲贊 7 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章