1. 程式人生 > 實用技巧 >回爐重造之重讀Windows核心程式設計-020-DLL高階技術

回爐重造之重讀Windows核心程式設計-020-DLL高階技術

第20章 DLL高階技術

20.1 DLL模組的顯示載入和符號連結

如果執行緒要呼叫DLL中的函式,DLL的檔案映像就必須對映到呼叫執行緒的程序地址空間中。有兩種方式:

  1. 讓執行緒只引用DLL中包含的符號,這樣當應用程式啟動時,載入程式就能夠隱含載入所需要的DLL。
  2. 第二張方法是指載入應用程式執行時載入DLL並顯示連結到需要的輸入符號。這種情況下,應用程式可以在執行時決定呼叫什麼函式。執行緒將DLL載入到程序的地址空間中後,就可以使用其中的所有函式。

20.1.1 顯式載入DLL模組

DLL可以被程序中的執行緒對映到程序的地址空間,只需呼叫下面的函式之一:

HINSTANCE LoadLibrary(PCTSTR pszDLLPathName);
HINSTANCE LoadLibraryEx(
  				PCTSTR pszDLLPathName, 
					HANDLE hFile,
					DWORD  dwFlags);

這兩個函式用於設法找出使用者系統上的DLL檔案,並對映到程序的地址空間中,它們的返回值HINSTANCE代表檔案映像的虛擬記憶體地址。如果返回NULL,那麼代表不能對映到空間中,錯誤的原因需要呼叫GetLasterror函式去查詢資訊。

LoadLibraryEx函式有額外的兩個引數,hFile供未來使用,而dwFlags現在也必須是0,或者是DONT_RESOLVE_DLL_REFERENCES、LOAD_LIBRARY_AS_DATAFILE和LOAD_WITH_ALTERED_SEARCH_PATH等標誌的組合。

  1. DONT_RESOLVE_DLL_REFERENCES:說明系統將DLL對映到程序的地址空間中。通常DLL被載入的時候,DllMain函式會觸發。然而當這個標誌被設定後,DllMain函式的觸發就被忽略了。
  2. LOAD_LIBRARY_AS_DATAFILE:和上面的DONT_RESOLVE_DLL_REFERENCES標誌相似,由於資料檔案的屬性,系統在載入DLL的時候就不打算執行DLL中的原始碼了。
  3. LOAD_WITH_ALTERED_SEARCH_PATH:用於改變查詢特定DLL的時候用使用的搜尋演算法。如果使用了這個標誌,函式就按照下面的順序搜尋檔案:
    1. pszDLLPathName引數中設定的路徑;
    2. 程序的當前路徑;
    3. Windows系統目錄;
    4. Windows目錄;
    5. PATH環境變數中列出的目錄;

20.1.2 顯示解除安裝DLL模組

當程序不再需要DLL的時候,DLL可以從程序的地址空間中顯式地解除安裝掉,使用下面的函式:

BOOL FreeLibrary(HINSTANCE hinstDll);

hinstDll引數必須被傳遞,用來表示將要解除安裝的DLL,它是呼叫LoadLibrary(Ex)的返回值。也可以用下面的函式解除安裝DLL:

VOID FreeLibraryAndExitThread(
	HINSTANCE hinstDll,
	DWORD	dwExitCode);

這個函式在Kernel32.dll中實現,方式並不稀奇:

VOID FreeLibraryAndExitThread(HINSTANCE hinstDll,	DWORD	dwExitCode)
{
  FreeLibrary(hinstDll);
  ExitThread(dwExtcode);
}

這其實並不高明,而可能會帶來很嚴重的問題。假如有一個DLL,而它是第一次被對映到程序的地址空間中的時候,DLL就會啟動了一個執行緒。執行緒做完工作後就呼叫FreeLibrary,解除安裝DLL,然後立即呼叫ExitThread。

但是如果分開呼叫FreeLibrary和ExitThread,又會有別的問題:FreeLibrary被呼叫後,包含ExitThread函式的DLL就被解除安裝了,ExitThread的呼叫又從何說起呢?這會引起訪問違規的。

而如果呼叫的是FreeLibraryAndExitThread,那麼即使DLL被正常解除安裝,下一個被執行的執行已經在Kernel32.dll中,而不是在剛剛被解除安裝的DLL裡。這就意味著執行緒可以繼續執行了。

實際使用時,載入和解除安裝DLL的這四個函式如果各自被重複呼叫多次後,影響的是其相對的引用計數的增減。引用計數的規則在這裡同樣有效。

如果想確定DLL是否已經被載入,可以使用下面的函式:

HINSTANC GetModuleHandle(PCTSTR pszModuleName);

有了DLL的控制代碼HINSTANCE,你甚至可以獲得DLL的完整路徑:

DWORD GetModuleFileName(
	HINSTANCE hinstModule,
	PTSTR     pszPathName,
	DWORD		  cchPath);

20.1.3 顯示地連結到一個輸出符號

獲取DLL中的一個符號地址,呼叫下面的函式就行:

FARPROC GetProcAddress(
	HINSTANCE hinstDll,
	PCSTR			pszSymbolName);

hinstDll是LoadLibrary(Ex)或者GetModuleHandle函式的返回值。引數pszSymbolName有兩種形式。

  1. 其一是以0結尾的字串,代表輸出符號的地址的名字,要注意的是這個字串的原始是PCSTR,這意味著函式接收的是ANSI字串,你不能將Unicode字串加給它。
  2. 其二是指明輸出符號的序號值。這種用法是假設你知道輸出符號對應的序號。由於Microsoft不推薦,所以這個方式不常用。

第一種方式比較慢,這是因為系統需要進行字串比較,並搜尋要傳遞的字串。還有這個函式的返回值是FARPROC,如果失敗是返回NULL的。

20.2 DLL的進入點函式

一個DLL擁有單獨的入口函式,系統會在不同的情況下呼叫這個函式。下面會分析這樣做的原因。進入點函式類似下面的情況:

BOOL WINAPI DllMain(HINSTANC hinstDll, DWORD fdwReason, pvoid fimpLoad) {
  switch(fdwReason) {
    case DLL_PROCESS_ATTACH:
      // The DLL is being mapped into the process's address space.
      break;
    case DLL_PROCESS_DETACH:
      // The DLL is being unmapped from the process's address space.
      break;
    case DLL_THREAD_ATTACH:
      // A Thread is beding created.
      break;
    case DLL_THREAD_DETACH:
      // A thread is exiting cleanly.
      break;
  }
  return TRUE; // Used only if  DLL_PROCESS_ATTACH
}

注意函式名是區分大小寫的。寫錯的話這個進入點函式就不會被呼叫,其中的一些初始化的工作也不能執行了。

hinstDll引數是DLL的例項控制代碼。和WinMain的hinstExe引數相似,標誌的是DLL檔案被對映到程序地址空間的虛擬記憶體地址。fimpLoad引數是個標誌,如果DLL是隱式載入的,這個引數是個非零值;如果DLL是顯示載入的,那麼這個值就是0。

fdwReason引數說明系統為何呼叫這個函式,它可以是DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH這4個值中的任何一個。

DLL中的函式同樣能被其他DLL呼叫。但是無論如何都不可以在DllMain函式中出現LoadLibrary(Ex),這會讓DLL很致命地被迴圈呼叫。

你的DllMain應該僅僅做些簡單的初始化工作(設定本地儲存器,建立核心物件和開啟檔案等),而避免呼叫User、Shell、ODBC、COM、RPC、和套接字函式(以及呼叫它們的函式),因為它們的DLL同樣有可能會還沒有初始化。

對於全域性或者靜態的C++物件,也可能有同樣的問題。

20.2.1 DLL_PROCESS_ATTACH通知

只有當DLL第一次被對映到程序的地址空間時,這個通知會被觸發。如果DLL重複被這樣做,作業系統只是遞增DLL的使用計數,不再觸發。這個通知可以用於一些關鍵的初始化操作,例如管理堆疊。而如果初始化取得成功後,返回值就是TRUE,否則就是FALSE。此後再接收到DLL_PROCESS_DETACH、DLL_THREAD_ATTACH或者DLL_THREAD_DETACH的話就會被忽略。

這樣的話,程序啟動的整個過程中涉及到的DLL如果帶有DLL_PROCESS_ATTACH的DllMain函式,那麼這些DLL的DLL_PROCESS_ATTACH就不能返回FALSE,否則就會失敗,地址空間中的DLL映像也會被解除安裝。

20.2.2 DLL_PROCESS_DETACH通知

DLL從程序的地址空間中被解除安裝時,系統將呼叫 DLL的DllMain函式,給它傳遞fdwReason
的值DLL_PROCESS_DETACH。當DLL處理這個值時,它應該執行任何與程序相關的清除操
作。例如管理堆疊。

注意,如果因為作業系統中的某個執行緒呼叫了TerminateProcess而使得程序終止執行,那麼系統是不會去呼叫帶有DLL_PROCESS_DETACH值的DllMain函式的。這意味著很多資源的回收工作就被跳過了,資料就會損失。只有在萬不得已之時,才能使用TerminateProcess。

20.2.3 DLL_THREAD_ATTACH通知

當在程序中建立執行緒時,系統要檢視對映到該程序的地址空間中的DLL,並檢視它們是否接收DLL_THREAD_ATTACH這個通知,因為這涉及到DLL對於執行緒的初始化操作。只有當所有的DLL都有機會處理這個通知的時候,系統才允許新執行緒開始執行新執行緒的執行緒函式 。

在DLL被引射到程序的地址空間之時,這個空間中可能已經有很多其他的DLL了。這樣的話新的DLL被對映進來之時,舊的DLL的DllMain函式中的DLL_THREAD_ATTACH通知是不會被觸發的,只有新的DLL裡的通知會。

另外,系統是不會為程序的主執行緒呼叫DllMain中的DLL_THREAD_ATTATCH通知的,而是DLL_PROCESS_DETACH。

20.2.4 DLL_THREAD_DETACH通知

讓執行緒終止的最佳方式是讓它的執行緒函式返回,這樣系統就可以利用ExitThread來撤銷執行緒。但是執行緒並不會立即被撤銷,而是取出已經對映進去的所有DLL,通知這些DLL的DllMain函式中的DLL_THREAD_DETACH,使得這些DLL使用的資源得以回收。

注意,DLL能夠阻止程序終止程序終止執行。例如,當DllMain接收到DLL_PROCESS_DETACH通知時,它就會進入一個無限迴圈。只有當每個DLL都已經完成對DLL_PROCESS_DETACH通知的處理之時,程序才會被終止執行。

如果因為系統呼叫了TerminateThread而終止了執行緒,那麼執行緒的DLL中有DLL_THREAD_DETACH的DllMain函式就不會得到呼叫。這意味著資料的丟失,所以只有在萬不得已的情況下才能使用這個手段。

20.2.5 順序呼叫DllMain

假如一個程序中有兩個執行緒A和B以及一個DLL(叫做some.dll),現在A和B都要各自建立新的執行緒C和D,2個新執行緒都要通知some.dll中DllMain函式中的DLL_THREAD_ATTACH所在的程式碼。在這種情況下,C和D對DllMain的呼叫就是有順序的了,就是如果C先訪問了DllMain,那麼D的訪問就被作業系統暫停,直到C的訪問結束,才輪到D訪問。

而這個特性通常會被忽略,如果C對DllMain訪問的過程中,出現了死迴圈或者WaitForXXX,就造成死鎖。

20.2.6 DllMain與CC++執行時庫

DllMain也是被別的函式呼叫的,和main函式相似。這是因為DllMain有可能會匯出一個C++類的物件,而這個類的初始化操作是在另一個DLL中,也就是CC++的執行時庫中。值得一提的是這個執行時庫一定是被靜態連結進來的。呼叫DllMain函式的函式實際上是_DllMainCRTStartup,此時就有條件初始化執行時庫、建立C++物件了。然後函式_DllMainCRTStartup就開始呼叫DllMain,DLL的工作開始了。

當DLL收到DLL_PROCESS_DETACH通知的時候,DllMain函式又被呼叫。在DllMain返回後,上面提到的匯出物件就會被銷燬,CC++執行時庫也會被解除安裝。

如果DllMain函式沒有被實現,那麼可以使用CC++執行時庫實現的程式碼:

BOOL WINAPI DllMain(HINSTANCE instDll, DWORD dwReason, PVOID fimpLoad) {
  if (dwReason == DLL_PROCESS_ATTACH) {
    DisableThreadLibraryCalls(hinstDll);
  }
  return TRUE;
}

如果你不實現DllMain的話,系統就會使用C++執行時庫的程式碼,假設你不關心DLL_THREAD_ATTACH和DLL_THREAD_DETACH這2個通知。為了提高效能,DisableThreadLibraryCalls函式會被呼叫。

20.3 延遲載入DLL

顧名思義,這個特性是讓DLL可以在需要其中的符號的時候再被載入。它的用處很大:

  1. 如果你的應用程式使用了很多DLL,那麼初始化它們的時間就比較長,因為需要把它們都載入到地址空間中。有了這個特性,應用程式啟動的速度就會提高很多。
  2. 如果要在舊版本的系統上使用應用程式時,有可能會造成DLL中的符號缺失的問題。這個時候延遲載入DLL的特性就會變得很有用。

要實現這個特性,需要的步驟有:

  1. 像平常一樣建立一個DLL和一個可執行模組。
  2. 修改兩個連結程式開關:
    1. /Lib:DelayImp.lib :告訴連結程式將一個特殊的函式 --delayLoadHeaper嵌入你的可執行模組。
    2. /DelayLoad:MyDll.dll
      1. 從可執行模組的輸入節中刪除MyDll.dll。這樣在程序被初始化的時候,就不會顯式地載入這個DLL。
      2. 將新的延遲輸入節(DelayImport,稱為.didata)嵌入可執行模組,以指明哪些函式正在從MyDll.dll中輸入。
      3. 通過轉移對--delayLoadHeaper函式的呼叫,轉換到對延遲載入函式的呼叫。

應用程式對延遲載入函式的呼叫實際上是對 --delayLoadHeaper函式的呼叫,這個函式可以知道呼叫LoadLibrary和GetProcAddrees。這個操作完成之後,將來呼叫函式就會轉向延遲載入的呼叫。

應用程式的啟動也需要很多DLL。當缺少了這些DLL的其中一個,應用程式將無法啟動,並顯示一條錯誤訊息。如果缺少的是延遲載入的DLL,應用程式就不檢查它是否存在了。只會在 --delayLoadHeaper函式中引發一個軟體異常條件,可以用SEH來追蹤。如果不追蹤在這個異常,程序將停止執行。

DLL找到了,應用程式需要的函式可能會沒有(因為版本之類的問題)。這樣也會發生一個異常,處理的方式和上面相同。

我們有兩個異常條件程式碼可以用,分別用來指明這個異常是缺少DLL(VcppException(ERROR_SEVERITY_ERROR、ERROR_MOD_NOT_FOUND)還是缺少函式(VcppException(ERROR_SEVERITY_ERROR、ERROR_PROC_NOT_FOUND))。下面的異常處理函式DelayLoadDllExceptionFilter用於查詢這兩個程式碼,如果沒找到過濾函式就返回EXCEPTION_CONTINUE_SEARCH。但是,如果其中一個找到了,那麼--delayLoadHeaper函式將提供一個(DelayLoadInfo)結構體的指標,結構體的定義如下:

typedef struct _DelayLoadInfo {
  DWORD           cb;           // Size of Structure
  PCImgDelayDescr pidd;         // Raw data (everything is here)
  FARPROC *       ppfn;         // Point to address of function to load
  LPCSTR          szDll;        // Name of dll
  DelayLoadProc   dlp;          // Name or ordinal of procedure
  HMODULE         hmodCur;      // hInstance of loaded library
  FARPROC         pfnCur;       // Actual function that will be called 
  DWORD           dwLastError   // Error received;
}DelayLoadInfo, *PDelayLoadInfo;

這個函式是由--delayLoadHelper函式來分配和初始化。該函式動態載入DLL並獲得被呼叫函式的地址的過程中,它將填寫這個結構的各個成員。成員szDll指向要載入的DLL的名字,要檢視的函式則在dlp中。pfnCur可以讓你瞭解DLL被載入到的記憶體地址,想知道到底發生了什麼錯誤可以使用dwLastError。不過異常已經告訴了你到底發生了什麼問題,這就不必要了。pfnCur是需要的函式的地址,不過它總是NULL,因為--delayLoadHelper無法找到該函式的地址。cb用於確定版本,pidd指向嵌入模組中包含延遲載入的DLL和函式的節。ppfn則是函式找到後應該放的地址。

若要解除安裝延遲載入的DLL,必須執行兩項操作:

  1. 當建立可執行檔案時,必須設定另一個連結程式開關(/delay:unload)
  2. 修改原始碼,在你想要解除安裝DLL時呼叫--FUnloadDelayLoadedDLL函式。
BOOL __FUnloadDelayLoadedDLL(PCSTR szDll);

/delay:unload連結程式開關告訴連結程式將另一個節放入檔案中。該節包含了你清除已經
呼叫的函式時需要的資訊,這樣它們就可以再次呼叫 --delayLoadHelper函式。當呼叫函式--FUnloadDelayLoadedDLL的時候,將要解除安裝的延遲載入的 DLL的名字傳遞給它。該函式進入檔案的未解除安裝節,並清除DLL中的所有函式地址,然後__FUnloadDelayLoadedDLL呼叫FreeLibrary,以便解除安裝DLL。

還有一些問題:

  1. 不要自己呼叫FreeLibrary來解除安裝DLL,否則函式的地址將不會被清除,這樣當下次試圖訪問DLL中的函式時就會發生訪問違規。
  2. 當呼叫--FUnloadDelayLoadedDLL傳遞的名字不應該包含路徑,名字的大小寫和傳遞給連結開關(/delay:unload)的引數相同,否則--FUnloadDelayLoadedDLL將會失敗。
  3. 如果不打算解除安裝延遲載入的DLL,那麼請不要設定這個開關。這樣可執行檔案的長度就會比較小。
  4. 如果 --FUnloadDelayLoadedDll沒有被呼叫,那麼什麼都不會發生,函式--FUnloadDelayLoadedDll只會返回FALSE。

最後一個特性,當--delayLoadHeaper函式執行時,它可以呼叫你提供的掛鉤函式。這些函式將接收--delayLoadHeaper函式的進度通知和錯誤通知。此外,這些函式可以過載DLL如何載入的方法以及如何獲取函式的虛擬記憶體地址的方法。

要實現這個行為特性,必須對你的原始碼最2件事情:

  1. 提供DliHook框架中類似的掛鉤函式。
  2. 啟動DliHook函式,對它進行修改。
  3. 將函式的地址告訴--delayLoadHeaper。

在DelayImp.lib靜態庫中定義了兩個全域性變數,pfnDliNotifyHookpfnDliFailureHook。它們的型別相同:

typedef FARPROC (WINAPI *PFNDLIHOOK)(
	unsigned dliNotify,
	PDelayLoadInfo pdli);

在DelayImp.lib檔案中這2個變數被初始化為NULL,它告訴–delayLoadHelper不要呼叫任何掛鉤函式。若要讓你的函式被呼叫,必須將這2個函式中的一個設定為掛鉤函式的地址。

--delayLoadHeaper實際上是和兩個函式一道執行的。它呼叫一個函式以便通知,呼叫另一個函式來報告失敗情況。由於它們的原型相同,所以可以通過建立單個函式並將兩個變數設定為指向我的一個函式,使工作簡單一些。

20.4 函式轉發器

這是DLL輸出節中的一個專案,用於將對一個函式FA的呼叫轉至另一個DLL中的另一個函式FB。當你的應用程式呼叫FA之時,應用程式就會連結FA所在的DLL。當應用程式被啟用的時候,載入程式就會載入FA所在的DLL,並看到FA其實轉發到了FB,系統中的任何地方都不存在FA。

如果呼叫GetProcAddress,這個函式就會檢視引數中的DLL的輸出節,確定HeapAlloc是個轉發函式,然後按遞迴方式呼叫GetProcAddress,查詢NTDLL.DLL中的輸出節中的RtlAllocateHeap函式。GetProceAddress(GetModuleHandle("Kernel32"), "HeapAlloc")

也可以利用DLL模組中的函式轉發器。最為容易的方式是使用一個pragma指令:

// Function forward to function in DllWork
#pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")

這個操作告訴連結程式,被編譯的DLL應該輸出一個名叫SomeFunc的函式。但是SomeFunc函式的實現實際上位於另一個名叫SomeOtherFunc的函式中,該函式包含在名叫DllWork.dll的模組中。這樣就可以為每一個轉發函式建立一個單獨的pragma程式碼行。

20.5 已知的DLL

有一些特殊的DLL,它們與其他的DLL並不不同,只是被作業系統做了特殊的處理,總是在同一個目錄中查詢這些DLL。這樣可以便於對這些DLL進行載入操作。參考下面的登錄檔關鍵字:

如你所見,這個關鍵字包含一組值的名字,這些名字是某些DLL的名字。每個值名字都有
一個數據值,該值恰好與帶有.DLLl副檔名的值名字相同。當LoadLibrary(Ex)被呼叫時,函式先產看是否傳遞了帶有.DLL副檔名的DLL作為引數。如果沒有傳遞,那麼函式將使用常規的搜尋規則來搜尋DLL。

如果設定了DLL的副檔名,那麼函式將刪除副檔名,然後搜尋登錄檔關鍵字,檢視是否存在相同的名字。如果沒有找到匹配的名字,便使用通常的搜尋規則。但是,如果搜尋到了DLL的名字的值,系統將查詢相關的值的資料,並設法用值的資料來載入DLL。系統也開始在登錄檔中的DllDirectory值資料指明的目錄中搜索DLL。按照預設的設定,DllDirectory值的資料是%SystemRoot%/System32

假設將下面的值新增給登錄檔關鍵字KnowDLL:

Value name: SomeLib
Value data: SomeOtherLib.dll

若你呼叫LoadLibrary(“SomeLib”);,系統將使用通常的規則查詢這個檔案。但是如果呼叫的是LoadLibrary(“SomeLib.dll);,系統將刪除副檔名,再檢視是否有匹配的值名字。

此時,系統將設法載入名字為SomeOtherLib.dll的檔案,而不是SomeLib.dll。它先系統目錄去%SystemRoot%/System32中查詢SomeOtherLib.dll。如果找到,就載入這個DLL,否則函式LoadLibrary(Ex)就執行失敗並返回NULL,同時對GetLastError的呼叫將返回2(ERROR_FILE_NOT_FOUND)。

20.6 DLL轉移

算是微軟應對版本變化的一個策略了。當Windows剛剛發展的時候,RAM和磁碟的空間都是十分寶貴的,所以CC++執行時類庫和MFC的DLL為了實現所有應用程式共享,就優先將這些類庫和DLL放在Windows的系統目錄。

但是隨著版本的更迭,應用程式會遇到新舊版本的類庫不匹配而導致無法正常執行的問題。現在硬碟的容量越來越大,RAM空間也有富餘, 因此Windows就改變了這個策略,用一個新的特性,使得應用程式可以強制的首先在應用程式的當前目錄載入檔案模組。只有在當前目錄下找不到的時候,才去別的目錄搜尋。

而實現這個這個特性的方式將是在你的應用程式的目錄放入一個檔案。檔案的內容可以忽略,但是內如必須是AppName.local。例如如果有一個可執行檔案的名字是SuperApp.exe,那麼轉移檔案的名字就是SuperApp.exe.local。

在系統內部,LoadLibrary(Ex)函式已經被修改,會先檢視是否存在這個檔案。如果應用程式的目錄中存在這個檔案,該目錄中的模組就已經被載入。如果不存在這個檔案,LoadLibrary(Ex)函式將正常執行。

這個特性對於已經註冊的COM物件來說是非常有用的,它能使應用程式將它的COM物件DLL放入自己的目錄。這樣註冊了相同COM物件的其他應用程式就不能干擾你的操作。

20.7 改變模組的位置

每個EXE或者DLL都有一個預設的載入基地址,EXE預設載入的基地址是0X00400000,而DLL的是0X00100000。但是如果所有模組都按照預設的基地址對映到RAM中,模組們就會發生重疊錯亂了。

既然Windows已經這麼成功,這個問題必然是有解決方案的。簡單的說就是為每個指定的檔案呼叫函式ReBaseImage:

BOOL ReBaseImage(
	PSTR CurrentImageName,			// Pathname of file to be rebase
	PSTR SymbolPath,					  // Symbol file path so debug info is accrate
	BOOL fReBase,							 // TRUE to actually do the work;FALSE to pretend
  BOOL fReBaseSysFileOk,			// FALSE to not rebase image system images
  BOOL fGoingDown, 						// TRUE to rebase the image can grow to an address
	ULONG CheckImageSize,				// Maximum size that image can grow to
  ULONG* pOldImageSize, 			// Receives original image size  
  ULONG* pOldImageBase,				// Receives original image base address
  ULONG* pNewImageSize, 			// Receives new image size 
  ULONG* pNewImageBase,				// Receives new image base address
  ULONG TimeStamp						  // New timestamp for image
);

當你執行Rebase程式,就會呼叫這個函式,為函式傳遞一組映像檔名時,它將執行如下的操作:

  1. 模擬創造一個程序的地址空間。
  2. 開啟通常被載入到這個地址空間的所有模組。
  3. 模擬改變各個模組在模擬地址空間中的位置,這樣各個模組就不會重疊。
  4. 對於已經移位的程式碼,它會分析該模組的移位節,並修改磁碟上的模組檔案中的程式碼。
  5. 更新每個移位模組的標頭檔案,以反映新的首選基地址。

對於生產工具,非常推薦使用Rebase程式。連微軟在銷售Windows作業系統之前,對作業系統提高的所有檔案都運行了Rebase程式,因此將它們對映到單個地址空間的話,所有的模組都不會重疊。

20.8 繫結模組

模組的移位可以提高系統的效能,但是這樣的辦法還有很多。可以將所有的模組繫結起來,使得應用程式可以儘可能快地初始化,儘可能使用少地使用儲存器。繫結一個模組的時候,就可以為這個模組的輸入節配備所有輸入符號的虛擬BindImage();地址。為了縮短初始化的時間和使用少的儲存器,這昂操作必須要在載入模組之前進行。

VS的另一個程式Bind.exe就是完成這項工作的一個工具,它的使用可以參考PlatformSDK文件。和Rebase相似,Bind.exe為每個指定的檔案重複呼叫BindImageEx函式:

BOOL BindImageEx(
	DWORD dwFlags;					// Flags giving fine control over the function
	PSTR pszImageName,			// Pathname of file to be bound
	PSTR pszDllPath,				// Search path used to locating image file
  PSTR pszSymbolPath,			//  Search path used to keep debug info accurate
  PIMAGEHELP_STATUS_ROUTINE StatusRoutine // Callback function
);

引數StatusRoutine是個回撥函式,BindImageEx會定期呼叫它,用來儘快連結程序:

BOOL WINAPI StatusRoutine(
	IMAGEHLP_STATUS_REASON Reason,		// Module/procedure not found, etc
  PSTR pszImageName,			// Pathname of file being bound
	PSTR pszDllPath,				// Pathname of DLL
  ULONG_PTR VA,						// Computed virtual address 
  ULONG_PTR Parameter			//  Additional info depending on Reason
);

當執行Bind程式,傳遞給它一個映像檔名時:

  1. 開啟指定映像檔案的輸入節。
  2. 開啟輸入節中列出的每個DLL,檢視它的標頭檔案以確定它的首選地址。
  3. 檢視DLL的輸出節中的每個輸入符號。
  4. 取出符號的RVA,並將模組的首選地址與它相加。將可能產生的輸入符號的虛擬地址吸入映像檔案的是輸入節中。
  5. 將某些輔助資訊新增到映像檔案的輸入節中,包括映像檔案繫結到的所有DLL模組的名字和這些模組的時間戳。

在程序執行的過程中,Bind程式有兩個重要的假設:

  1. 當程序初始化的時候,它需要的所有DLL實際上都被載入到它們的首選基地址中。可以使用Rebase工具來確保這一點。
  2. 從繫結操作執行開始,DLL的輸出節中引用的符號的位置上一直沒有改變。載入程式通過將每個DLL的時間戳與上面第5個步驟中儲存的時間戳比對,以核實這個情況。

如果核實失敗,載入程式就需要通過人工來修改可執行模組的輸入節,就像它通常所做的那樣。但是如果載入程式發現模組已經連線,需要的DLL已經載入到它們的首選基地址,而且時間戳也匹配,那麼載入程式實際上不需要做什麼,讓應用程式只管執行即可。

連來自系統的頁檔案的儲存器都不必使用。

看起來已經很完美。但是什麼時候去連線模組呢?在開發的過程中,可以使用系統的DLL繫結,但是這些DLL不一定是使用者會安裝的。更有甚者,如果是雙系統的情況下,Win7和Win10使用的DLL肯定有差別。