程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 《Windows核心編程系列》談談DLL高級技術

《Windows核心編程系列》談談DLL高級技術

編輯:關於C語言

 

本篇文章將介紹DLL顯式鏈接的過程和模塊基地址重定位及模塊綁定的技術。

 

      第一種將DLL映射到進程地址空間的方式是直接在源代碼中引用DLL中所包含的函數或是變量,DLL在程序運行後由加載程序隱式的載入,此種方式被稱為隱式鏈接。

 

      第二種方式是在程序運行時,通過調用API顯式的載入所需要的DLL,並顯式的鏈接所想要鏈接的符號。換句話說,程序在運行時,其中的一個線程能夠顯式的將該DLL調用到進程地址空間中,並得到DLL中某函數的在進程地址空間的虛擬地址,然後調用該函數。此種方式被稱為顯式鏈接。

 

      注意:顯式載入某DLL時,不需要該dll的Lib文件,且exe文件中並不包含該dll的導入表。

 

顯示載入DLL模塊的步驟:

 

    線程可以調用LoadLibrary將一個DLL映射到進程地址空間。

 

 

 

 

HMODULE LoadLibrary(PCTSTR pszDLLPathName); 

 

     該函數會試圖對程序想載入的DLL進行定位,並試圖將該DLL映射到調用進程的地址空間中。返回是DLL在調用進程的虛擬地址。即模塊的句柄。如果無法將DLL載入到進程地址空間中返回值為NULL.

 

     與它類似的另一個函數

 

 

HMODULE LoadLibraryEx(PCTSTR pszDLLPathName,HANDLE hFile,DWORD dwFlags); 

 

      也可以實現將DLL載入到進程地址空間的目的。具體請參考MSDN。

 

     加載後如果程序不再需要該DLL,可以調用FreeLibrary將DLL從進程地址空間中卸載:

 

 

BOOL FreeLibrary( HMODULE  hInstDll ); 

 

    也可以調用FreeLibraryEx卸載某DLL。

 

    以下函數不僅具有從進程地址空間卸載某DLL的功能,還能退出調用線程:

 

 

VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode) 

 

 

 FreeLibrary(HMODULE hInstDll); 

 

 ExitThread(dwExitCode); 

 

 

       剛見到時或許你會覺得它很多余。考慮下面的情形:

 

       我們調用一個DLL,該DLL中的代碼會創建一個線程,當此線程完成工作後,可以調用FreeLibrary和ExitThread將DLL從進程地址空間中卸載,並終止自己。由於線程是由DLL創建的,線程執行的代碼也在DLL中,當線程調用FreeLibrary將它所在的DLL卸載的時候,它後續要執行的代碼已不再進程地址空間中了,試圖執行不存在的代碼可能會導致訪問違規,導致進程被終止。

 

       如果線程調用FreeLibraryAndExitThread,此函數在Kernel32.dll中,FreeLibraryAndExitThread函數調用FreeLibrary將線程函數所在的DLL卸載後,其所屬DLL Kernel32.dll仍在進程地址空間內,FreeLibraryAndExitThread函數繼續執行調用ExitThread,後續代碼仍然存在,不會導致訪問違規。

 

       每個DLL在進程中都有一個使用計數。LoadLibrary(Ex)會增加其計數,FreeLibrary(Ex)和FreeLibraryAndExitThread會遞減其計數。例如:當程序第一次調用LoadLibrary來載入一個DLL時,系統會將此DLL映射到進程地址空間中,並將此DLL的使用計數加一。如果線程後來再次調用LoadLibrary(Ex)時,系統不會將此DLL再次映射到進程地址空間,僅僅遞增此DLL的使用計數。為了從進程地址空間中撤銷對該DLL的映射,線程必須調用FreeLibrary(Ex)兩次。第一次是將此DLL的使用計數減為1,第二次減為0。當系統發現某DLL的使用計數已經為0時,會從進程地址空間卸載此DLL。此時如果線程試圖顯式調用DLL中的函數將會導致訪問違規。

 

     系統會在每個進程中為DLL維護一個使用計數,在本進程調用LoadLibrary僅僅是增加DLL在本進程的使用計數。如果進程A中的一個線程執行了LoadLibrary("Mydll.dll");進程B的某一線程也調用LoadLibrary("Mydll.dll");那麼該DLL會被映射到A,B兩個進程空間中去,且在A和B進程的使用計數都為1。

 

調用FreeLibrary("Mydll.dll");也僅僅是遞減DLL在本進程內的使用計數。

 

 

 

 

HMODULE  GetModuleHandle(PCTSTR pszModuleName); 

 

      該函數可以用來檢測某DLL是否被映射到了進程地址空間。如果返回值為NULL,則此DLL未被載入。

 

     當給pszModuleName傳NULL時,函數會返回應用程序可執行文件的句柄。

 

 顯式鏈接導出符號

 

       顯式載入某個DLL後,線程可以通過調用以下函數來得到它要引用的符號的地址。

 

    

 

 

FARPROC GetProcAddr(HMODULE hInstDll, PCSTR pszSymbolName); 

 

         hInstDll標識導出符號所在的DLL的句柄。它是LoadLibrary(Ex),或是GetModuleHandle所返回的句柄。

 

     pszSymbolName用於標識導出符號。

 

         pszSymbolName可以有兩種形式:

 

         第一種:用符號名來指定我們想要得到哪個符號的地址。

 

        如:FARPROC pfn=GetProcAddress(hInstDll,"MyProc");

 

        它是以0結尾的字符串。要注意此字符串是ANSI類型的。因為編譯器、鏈接器始終都是將符號的名稱以ANSI字符串的形式保存在DLL的導出段。

 

         第二種:用序號來指定我們想要那個符號的地址。

 

         如:FARPROC pfn=GetProcAddress(hInstDll,MAKERESOURCE(2));

 

         這種方法假定我們知道某個導出符號在某DLL中的序號為2。應該明確的是Microsoft強烈反對使用序號。

 

使用序號的形式要比使用字符串速度慢,因為系統需要對一字符串標識的符號名進行字符串比較。使用第二種方法即使該序號並沒有與任何導出函數相對應,GetProcAddress也會返回非NULL值。其實這個地址是無效的,訪問此地址可能會導致訪問違規。

 

      注意:使用GetProcAddress返回的函數指針來調用函數之前,需要將它轉換成與函數簽名相匹配的類型。

 

     例如:

 

    

 

 

typedef void (CALLBACK *PFN_DUM_MOUDLE)(MODULE hModule); 

 

      它是與void DynamicDumpModule(HMODULE hModule)函數相對應的函數相同。

 

      動態調用某DLL導出函數的例子:

 

 

<span style="font-size:18px;"> PFN_DUMPMODULE pfnDumpModule=(PFN_DUMPMODULE)GetProcAddress(hDll,"DumpModule"); 

 

If(pfnDumpModule!=NULL) 

 { 

    pfnDumpModule(hDll); 

 } 

 

;/span> 

 

DLL的入口點函數

 

         一個DLL可以有一個入口點函數,系統會在不同的時候調用這個函數。這些調用是通知性質的,通常被DLL用來執行與進程或線程有關的初始化和清理工作。

 

       如果不需要執行這些操作,可以不必再源代碼中不實現此函數。

 

       如果需要DLL接受這些通知,就應該按照如下的格式來實現該函數。

 

   

 

 

<span style="font-size:18px;">Bool WINAPI DllMain(HINSTANCE hInsDll,DWORD fdwReason,PVOID fImpLoad) 

 

 

     Swith(fdwReason) 

 

    { 

 

        Case DLL_PROCESS_ATTACH: 

 

   

 

             //DLL被映射到進程地址空間是,執行此處代碼。 

 

                Break; 

 

         Case DLL_THREAD_ATTACH: 

 

             //線程被創建的時候執行。 

 

                 Break; 

 

         Case DLL_THREAD_DETACH: 

 

               //線程終止運行時執行。 

 

                   Break; 

 

         Case DLL_PROCESS_DETACH: 

 

                //DLL被卸載的時候執行。 

 

                    Break; 

 

       } 

 

</span> 

 

          hInstDll是該DLL實例的句柄。它是DLL文件被映射到進程地址空間的虛擬地址。通常將這個參數保存在全局變量中。這樣在DLL的其他導出函數中就可以使用。

 

        如果DLL是被隱式載入的,fImpLoad為非零值,顯式的話fImportLoad為0。

 

        fdwReason表示系統調用入口點函數的原因。它是switch語句的參數。可以是上述四個值。分別表示四種情況。後續將會詳細介紹每一種情況。

 

          注意:DLL使用DllMain對自己進行初始化。DllMain執行的時候,其他DLL的可能還未被初始化。這意味著我們應該避免在DllMain中調用從其他DLL中導出的函數。

 

DLL_PROCESS_ATTACH通知 www.2cto.com

 

         當系統第一次將一個DLL映射到進程地址空間是,會調用DllMain函數,並給fdwReason傳入DLL_PROCESS_ATTACH。注意:只有在該DLL是第一次被調用到進程地址空間中時,才會調用DllMain。如果以後再次調用LoadLibrary(Ex)時,OS僅僅是遞增該DLL在此進程的使用計數,並不會再次調用DllMain。

 

         當DLL在處理DLL_PROCESS_ATTACH時,應該根據需要執行與進程相關的初始化。如DLL中包含一些函數,需要使用自己的堆,可以在進程加載時執行一些堆的初始化工作。

 

處理DLL_PROCESS_ATTACH時,DllMain的返回值表示DLL的初始化是否成功。如初始化成功,應返回TRUE,否則應返回false。

 

       下面來看看DllMain調用的時機:

 

        創建新進程時,系統為該進程分配地址空間,並將exe可執行文件和所需要的DLL映射到進程地址空間。然後創建主線程,並用主線程來調用每個DLL的DllMain函數,同時傳入DLL_PROCESS_ATTACH。當所有已映射的DLL完成對該通知的處理後,系統會讓主進程執行可執行模塊的C/C++運行庫的啟動代碼。然後執行可執行模塊的入口點函數(_tmain或_tWinMain)。如果任意一個DLL的DllMain返回false,就說明初始化失敗,系統會將所有文件映像從地址空間中清除,向用戶顯示錯誤信息。

 

顯式載入DLL的過程:

 

         進程調用LoadLibrary(Ex),該函數對DLL進行定位,並將該DLL映射到進程地址空間。然後會讓調用LoadLibrary(Ex)的線程調用DllMain函數,並傳入DLL_PROCESS_ATTACH。當DLL的DllMain函數完成了對通知的處理後,系統會讓LoadLibrary返回。這樣線程就可以繼續執行。

 

         注意:DllMain是在進程調用LoadLibrary(Ex)的時候調用的。它返回到LoadLibrary(Ex)函數內。

 

 

 

      未完待續。。。。。

 

 

 

      《參考自windows核心編程》第五版第四部分。以上僅僅是個人總結,如有纰漏請不吝賜教!

 

摘自 ithzhang的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved