一、前言

在前一段時間,我遭遇了一個現象詭異的Bug,最后原因歸結 為在DllMain里錯誤地調用了FreeLibrary(在本文最后對此Bug有詳細的解釋)。MSDN里關于禁止在DllMain里調用 LoadLibrary和FreeLibrary的解釋過于含糊不清,所以我重溫了一遍Russ Osterlund的"Windows 2000 Loader"一文,并仔細閱讀了泄漏的Win2000源代碼的相關部分。按照我一貫的習慣,我的閱讀過程形成了我這篇文章的主體。自從我2000年寫了"ATL接口映射宏詳解" 以來,我還沒寫過這么大塊頭的文章。我不知道有多少人耐著性子看完了"ATL接口映射宏詳解",我猜想這篇文章的命運也不會比它的前輩好多少。在這個技術 更新越來越快的年代里,人們會對這種陷入實現細節的文章感到厭煩,而我自己在若干年后可能也不會有耐心和勇氣面對它,但文章最后對幾個問題的解釋也還是有 實用價值的,另外尋根究底的精神也總是應該存在的。

二、準備工作

工具
用SourceInsight看Win2000的源代碼會比較爽。
WinDbg是調試用的神兵利器,它能顯示比VC更多的調試信息,以及一些內部的數據結構,當然你需要先安裝與你的OS相符合的調試符號。
GFlag.exe可以設置輸出Loader Snap信息,它和WinDbg一起,都在Debugging Tools for Windows包里。
ModuleList是我寫的一個小工具,與本文相得益彰。

知識
在開始跟隨我的腳步之前,你至少應該先閱讀一下"win2k\private\net\sockets\winsock2\dll\include\llist.h"文件。在這里定義一些非常重要的宏和結構,包括:
???????? LIST_ENTRY、FIELD_OFFSET、CONTAINING_RECORD以及雙向鏈表添加刪除結點的幾個宏。
雖然在好幾個文件里有這幾個宏的相同定義,但顯然這個文件是最好的,因為它有非常詳細的注釋。理解LIST_ENTRYCONTAINING_RECORD非常關鍵,這種簡單高效又富于技巧性的雙向鏈表結構遍布于Win2000源碼的各個角落之中,包括與本文密切相關的PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY結構。(在ModuleList中給出了這兩個結構的定義)

瀏覽一遍Russ Osterlund的"Windows 2000 Loader"也是非常必要的,為了避免重復,我省略了一些內容。不過他的文章也很長,份量很重,看完它需要花費很多心力。

另外你還需要有相當多的PE方面的知識,特別是Import和Export的部分,還有Forward API和binding的概念。

如 果做完以上準備工作之后,閱讀本文仍然有困難的話,那么非常遺憾,我的寫作能力還不足以讓你跳過源代碼,還是請你先閱讀過win2000的源代碼再回來 吧,畢竟代碼才是最好的文檔。(win2000源代碼里的注釋是1990年,而Russ Osterlund在2002年給出的偽代碼與它高度相似,這是否說明我們現在用的Windows Loader的主干代碼在十幾年前就已經確立了呢?這不禁讓我有一絲莫名的激動。)

三、Process Initialize

LdrpInitialize is called as a User-Mode APC routine as the first user-mode code executed by a new thread.不過我們從LdrpInitializeProcess開始研究就已經足夠了,并且本文只關注與Dll loader相關的部分。

LdrpInitializeProcess的功能:
This function initializes the loader for the process.
????This includes:
????????- Initializing the loader data table
????????- Connecting to the loader subsystem
????????- Initializing all staticly linked DLLs

(1) 初始化Hash table

				for(i=0;i<LDRP_HASH_TABLE_SIZE;i++) {
InitializeListHead(&LdrpHashTable[i]);
}

LdrpHashTable是全局變量:
#define LDRP_HASH_TABLE_SIZE 32
#define LDRP_HASH_MASK (LDRP_HASH_TABLE_SIZE-1)
#define LDRP_COMPUTE_HASH_INDEX(wch) ( (RtlUpcaseUnicodeChar((wch)) - (WCHAR)'A') & LDRP_HASH_MASK )
LIST_ENTRY LdrpHashTable[LDRP_HASH_TABLE_SIZE]; // Hash表,每一項是一個雙向鏈表結構
這里采用的是非常簡單的Hash算法。

(2) 得到LdrpKnownDllPath

    LdrpKnownDllObjectDirectory是named object"\\KnownDlls"的句柄。
如果打開該對象不成功,則LdrpKnownDllPath默認的就是系統目錄,比如:"c:\winnt\system32"
如果打開該對象成功,則在該directory下有一item "KnownDllPath"(a symbolic-link object),用這個值初始化LdrpKnownDllPath。

LdrpKnownDllObjectDirectory和LdrpKnownDllPath是全局變量:
HANDLE LdrpKnownDllObjectDirectory;
UNICODE_STRING LdrpKnownDllPath;

如果不能成功得到LdrpKnownDllPath,則會退出LdrpInitializeProcess函數。
LdrpKnownDllPath將在后面的LdrpCheckForKnownDll函數中被用到。

(3) 初始化Peb->Ldr,參見ModuleList中給出的定義。

				// 在進程堆上為Ldr分配空間
				
Peb->Ldr = RtlAllocateHeap(Peb->ProcessHeap, MAKE_TAG( LDR_TAG ), sizeof(PEB_LDR_DATA));

Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
Peb->Ldr->Initialized = TRUE;
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);

(4) 為process image分配第一個loader data table entry,初始化,并加入到list中

    LdrDataTableEntry = LdrpImageEntry = LdrpAllocateDataTableEntry(Peb->ImageBaseAddress);
... 初始化各個成員 ....
LdrpInsertMemoryTableEntry(LdrDataTableEntry); // 將該entry加入到list of loaded modules for this process
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED;

LdrpInsertMemoryTableEntry函數比它的名字包含了更多的含義,它不僅insert into LoadOrderModuleList和MemoryOrderModuleList,
還insert into HashList:
ULON i = LDRP_COMPUTE_HASH_INDEX(LdrDataTableEntry->BaseDllName.Buffer[0]);
InsertTailList(&LdrpHashTable[i],&LdrDataTableEntry->HashLinks);
InsertTailList(&Ldr->InLoadOrderModuleList, &LdrDataTableEntry->InLoadOrderLinks);
InsertTailList(&Ldr->InMemoryOrderModuleList, &LdrDataTableEntry->InMemoryOrderLinks);

(5) 為ntdll.dll分配第二個loader data table entry,初始化,并加入到list中
對于任何一個進程,ntdll.dll都是第一個被處理的DLL。

    LdrDataTableEntry = LdrpAllocateDataTableEntry(SystemDllBase); // 即ntdll.dll的基地址
... 初始化各個成員 ....
與Process image不同,ntdll.dll會被加入到初始化鏈表中:
InsertHeadList(&Peb->Ldr->InInitializationOrderModuleList,
&LdrDataTableEntry->InInitializationOrderLinks);
這也是InInitializationOrderModuleList長度總比InLoadOrderModuleList和InMemoryOrderModuleList多1個的原因。

ntdll.dll 的一個有趣的事是它的入口點EntryPoint為NULL,所以不會調用_DllMainCRTStartup,所以不會有 LDRP_PROCESS_ATTACH_CALLED標志。用ModuleList.exe會發現所有進程里的ntdll.dll都是如此。
ntdll.dll的另一個特殊之處是它的LoadCount初始為-1,意味著LoadCount永遠不會改變。

(6) 加載Process引用的DLLs

    LdrpWalkImportDescriptor(LdrpDefaultPath.Buffer, LdrpImageEntry);
LdrpImageEntry是在前面已經分配過的全局的Process Image的loader data table entry.

LdrpWalkImportDescriptor
????????is a recursive routine which walks the Import Descriptor Table and loads each DLL that is referenced.

				if (Bound Imports Descriptor Table存在)
{
while (遍歷每一個IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT結構)
{
調用LdrpLoadImportModule(...)裝載綁定的Dll,得到該dll的loader data table entry;
如果成功并且該dll是第一次被load,則調用InsertTailList將其加入到InInitializationOrderModuleList的末尾。

if (該dll的時間戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;

while (處理該dll的每一個forwarder dll)
{
調用LdrpLoadImportModule(...)裝載forwarder dll;
如果成功并且該dll是第一次被load,則調用InsertTailList將其加入到InInitializationOrderModuleList的末尾。

if (不成功,或者該dll的時間戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;
}

if (StaleBinding == TRUE)
{
Find the unbound import descriptor that matches this bound import descriptor
如果沒找到,則返回STATUS_OBJECT_NAME_INVALID,退出

調用LdrpSnapIAT(...)修正IAT表。
}
/* 這一部分的代碼現在肯定已經有所變化。通過Russ Osterlund的例子可以發現,如果使用LoadLibrary來load一個Forwarder DLL,
使用GetProcAddress來使用一個Forwarder Function,那么LoadLibrary不會加載Forwarded Dll,只有在GetProcAddress之后,
才會加載Forwarded Dll。這是一種類似Delay-load的機制。
*/

}
}
elseif (Regular Imports Descriptor Table存在)
{
while (遍歷每一個IMAGE_DIRECTORY_ENTRY_IMPORT結構)
{
調用LdrpLoadImportModule(...)裝載imported dll,得到該dll的loader data table entry;

if (this dll has been bound // 通過timestamp判斷,See PE specifications 6.4.1
&& the import date stamp matches the date time stamp in the export modules header
&& and the image was mapped at it's prefered base address)
{
// do nothing
}
else
{
調用LdrpSnapIAT(...)修正IAT表。
}

如果Dll是第一次被load,則調用InsertTailList將其加入到InInitializationOrderModuleList的末尾。
}
}

以下是在不同的情況下輸出的Loader Snap信息。
Bound成功的例子:
LDR: KERNEL32.dll bound to NTDLL.DLL
LDR: KERNEL32.dll has correct binding to NTDLL.DLL
Bound不成功的例子:
LDR: SHELL32.dll has stale binding to SHLWAPI.DLL
LDR: Stale Bind SHLWAPI.DLL from SHELL32.dll
Bound里有forward成功的例子:
LDR: GDI32.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: GDI32.dll has correct binding to NTDLL.DLL
Bound里有forward不成功的例子:
LDR: WINMM.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: WINMM.dll has stale binding to NTDLL.DLL
LDR: Stale Bind KERNEL32.DLL from WINMM.dll

LdrpLoadImportModule: load Imported Dll

    (1) 調用LdrpCheckForLoadedDll(...)檢查該Dll是否已經被load。
(2) 若沒有,則調用LdrpMapDll(...)將其映射到進程地址空間。
(3) 遞歸調用LdrpWalkImportDescriptor(...)。
LdrpCheckForLoadedDll和LdrpMapDll這兩個函數留到后面再講。

LdrpSnapIAT:snaps the Import Address Table for this Imported Dll,overwrites each IAT entry with the actual address of the imported function.

    (1) 通過IMAGE_DIRECTORY_ENTRY_EXPORT得到imported dll的Export Directory指針和大小,它將在LdrpSnapThunk函數中使用。
(2) 通過IMAGE_DIRECTORY_ENTRY_IAT得到IATs表的地址和大小。(每一個imported dll的IAT表在內存中都是連續排列的)
這是一種簡便的方法,一下子把整個IAT表的區域的屬性都改了,避免了每snap一個thunk修改一次。
(3) 修改IATs的內存保護屬性為PAGE_READWRITE。
(4) if (snap forwarded entries only)
{
while (找到每一個forwarder function的thunk)
調用LdrpSnapThunk(...)
}
else
{
while (找到Import Table里的每一個thunk)
調用LdrpSnapThunk(...)
}
(5) 恢復IATs原始的內存保護屬性。
(6) 調用NtFlushInstructionCache。這是有必要的,因為IATs一般都在代碼段。

LdrpSnapThunk: snaps a thunk using the Imported Dll's Export Section data.

    (1) if (snap is by ordinal)
{
得到OrindalNumber: = (USHORT)(OriginalOrdinalNumber - ExportDirectory->Base);
}
else
{
如果HintIndex匹配函數名,則可以直接使用它:OrdinalNumber = NameOrdinalTableBase[HintIndex];
否則調用LdrpNameToOrdinal(...)在Name Table中二分查找,然后在NameOrdinal Table得到對應OrdinalNumber
}
(2) 根據得到的OrdinalNumber,在Export Address Table(EAT)中找到對應的API的偏移地址。
該偏移地址再加上Dll的基地址就是該函數在內存中的實際地址。
然后用它更新IAT Thunk Entry。
(3) (參考PE specifications中的6.3.2節)
if (函數地址在export section內)
{
說明這個函數是一個Forwarder Function,那么上面得到的該函數的地址實際上指向一個ASCII string,
形式如:"NTDLL.RtlAllocateHeap" (by name) 或者 "MYDLL.#27" (by ordinal) 。

從這個字符串中解析出Forwarded Dll的名字,然后調用LdrpLoadDll(...)函數裝載它。
然后調用LdrpGetProcedureAddress(...)函數得到函數的實際地址,并更新IAT Thunk。
}

(7) 調用LdrpUpdateLoadCount增加process image及它引用的dll的引用計數
LdrpUpdateLoadCount:遞歸函數,增加或減少Dll以及它引用的所有Dll的引用計數:

				if (Module is loading)
{
設置相應的LDRP_LOAD_IN_PROGRESS標志,該標志表示dll正在被loading,將在LdrpClearLoadInProgress(...)被清除。
}
else// (Module is unloading)
{
設置相應的LDRP_UNLOAD_IN_PROGRESS標志,該標志表示dll正在被unloading,將在LdrUnloadDll(...)中被清除。
}

if (Bound Imports Descriptor Table存在)
{
while (遍歷每一個IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT結構)
{
調用LdrpCheckForLoadedDll(...)檢查imported dll是否已經被加載。
if (該imported dll的引用計數 != -1)
{
if (reference)
引用計數加1
else// dereference
引用計數減1
}
對這個imported dll遞歸調用LdrpUpdateLoadCount(...)
}
}
elseif (Regular Imports Descriptor Table存在)
{
while (遍歷每一個IMAGE_DIRECTORY_ENTRY_IMPORT結構)
{
調用LdrpCheckForLoadedDll(...)檢查imported dll是否已經被加載。
if (該imported dll的引用計數 != -1)
{
if (reference)
引用計數加1
else// dereference
引用計數減1
}
對這個imported dll遞歸調用LdrpUpdateLoadCount(...)
}
}

(8) Lock the loaded DLLs to prevent dlls that back link to the exe to cause problems when they are unloaded.

				while (從前向后遍歷InLoadOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
LoadCount = -1 ;
這表明進程的每一個static link的dll的LoadCount都為-1。
從上面的LdrpUpdateLoadCount的偽代碼可以看出,LoadCount為-1標志著該dll的引用計數永遠不會改變,
不會因為LoadLibrary和FreeLibrary而增加或減小。
}

(9) 此時進程隱式鏈接的DLLs都已經映像到內存中

				if (the process is being debugged)
{
DbgBreakPoint() ; // 這就是著名的Loader Breakpoint。
}

Debugger的作者需要注意的是,在Loader Breakpoint之前,staticly linked dlls雖然都已經被load,但并沒有被初始化(意即沒有調用_DllMainCRTStartup)。 用WinDbg的!dlls命令,或者我的ModuleList程序都可以看出這點:
這些static link DLL的標志均為:LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_LOAD_IN_PROGRESS。

(10)調用LdrpRunInitializeRoutines,初始化每個dll。
LdrpRunInitializeRoutines:調用每一個已經被映射到內存但又沒初始化的Dll的Entry Point。

    (1) 調用LdrpClearLoadInProgress(...),清除LDRP_LOAD_IN_PROGRESS,并返回需要調用初始化函數的模塊個數。
NumberOfRoutines = LdrpClearLoadInProgress();
(2) 在進程堆上創建一個數組,其成員是將要調用初始化函數的模塊所對應的PLDR_DATA_TABLE_ENTRY指針。
PLDR_DATA_TABLE_ENTRY *LdrDataTableBase = RtlAllocateHeap( , , NumberOfRoutines * sizeof(PLDR_DATA_TABLE_ENTRY)) ;
(3) while (從前向后遍歷InInitializationOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不為NULL && 沒有設置LDRP_ENTRY_PROCESSED標志(即entry hasn't been processed))
LdrDataTableBase[i++] = LdrDataTableEntry; // 將其將加入LdrDataTableBase中
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED; // 注意此時還沒有調用Entry Point函數
}
(4) while (遍歷LdrDataTableBase數組中的每一項)
{
判斷是否需要"BreakOnDllLoad" ;
我對BreakOnDllLoad沒什么興趣,就此略過。感興趣的話可以看看Matt Pietrek的"Under the Hood", 1999-09

if (InitRoutine) // 如果需要初始化
{
if (the DLL has TLS data)
調用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;

調用LdrpCallInitRoutine(,,DLL_PROCESS_ATTACH,)函數,一般是調用Dll的入口點函數_DllMainCRTStartup。
LdrpCallInitRoutine是用匯編寫的,不過并無特殊之處。只是在Call指令之前調用
mov esi,esp ; save the stack pointer in esi across the call
在Call結束后調用
mov esp,esi ; restore the stack pointer in case callee forgot to clean up
不知道這種設計有什么特別的好處?

LdrDataTableEntry->Flags |= LDRP_PROCESS_ATTACH_CALLED; // 標識完成初始化

if (Entry Point函數返回FALSE)
退出,返回STATUS_DLL_INIT_FAILED; // 這說明如果有一個Dll初始化失敗,則退出整個加載過程
}
}
(5) if (the process image has tls)
調用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;

LdrpClearLoadInProgress:清除LDRP_LOAD_IN_PROGRESS標志

    (1) count = 0 ;    // 初始化計數器
(2) while (從前向后遍歷InInitializationOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
清除LDRP_LOAD_IN_PROGRESS標志;
if (EntryPoint不為NULL && 沒有設置LDRP_ENTRY_PROCESSED標志(即entry hasn't been processed))
++count ;
}
(3) return count ;

四、Process Shutdown

下面我們開始研究進程結束時Dll是如何卸載的,從下面的堆棧中可以確定我們的旅程將從LdrShutdownProcess開始:

    ntdll!LdrShutdownProcess
KERNEL32!ExitProcess+0x51
Test!doexit+0xd5 [crt0dat.c @ 392]
Test!exit+0x10 [crt0dat.c @ 279]
Test!mainCRTStartup+0xf8 [crt0.c @ 212]
KERNEL32!BaseProcessStart+0x3d

LdrShutdownProcess
??????This function is called by a process that is terminating cleanly.
??????It's purpose is to call all of the processes DLLs to notify them that the process is detaching.

    (1) 沿著初始化方向的反方向
while (從后向前遍歷InInitializationOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不為NULL && 設置了LDRP_PROCESS_ATTACH_CALLED標志(即the dll has been initialized))
{
if (the DLL has TLS data)
調用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;

調用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函數
}
}
(2) if (the process image has tls)
調用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;

原來進程結束時只是依次調用Dll的DllMain函數,并沒有把它從內存中卸載(UnmapView)。

五、LoadLibraryEx

進 程初始化里是加載靜態鏈接的DLLs,下面要學習動態加載Dll(LoadLibraryEx)的代碼。關于這部分內容,Russ Osterlund的"Win2000 Loader"里有非常詳盡的描述,我也沒必要重復。這里我只寫出LdrpCheckForLoadedDll和LdrpMapDll兩個函數的算法思 想:

LdrpCheckForLoadedDll:
??????This function scans the loader data table looking to see if the specified DLL has already been mapped into the image. If
??????the dll has been loaded, the address of its data table entry is returned.

    (1) if (StaticLink) 
{
在哈希表LdrpHashTable中查找Dll,如果找到則返回TRUE,否則則返回FALSE
}
(2) if (Dll的名字中沒有包含路徑)
{
StaticLink = TRUE;
返回(1)
}
(3) 調用RtlDosSearchPath_U(...)得到Dll的全路徑
(4) while (從前向后遍歷InLoadOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
// when we unload, the memory order links flink field is nulled.
// this is used to skip the entry pending list removal.
if ( !Entry->InMemoryOrderLinks.Flink )
continue;
關于InMemoryOrderLinks.Flink為NULL的情況,留到LdrUnloadDll再講。

比較FullDllName,如果匹配,則退出循環
}
(5) if (沒找到)
{
這部分代碼不是很明白,我也不是很關心。大概意思是把Dll映射到內存中,然后再遍歷InLoadOrderModuleList表,
比較TimeDateStamp,SizeOfImage以及整個file header和optional header,如果都匹配,則說明找到,成功返回。
}

我不是很明白這個函數的代碼為什么這么寫,Russ Osterlund的代碼也不是很清晰。
如果是我寫,我就只比較LdrpHashTable哈希表。如果Dll沒有包含路徑,就比較BaseDllName,否則就比較FullDllName。
為什么還要找InLoadOrderModuleList,它和LdrpHashTable有什么不一致嗎?

LdrpMapDll:This routine maps the DLL into the users address space.

    (1) if (LdrpKnownDllObjectDirectory != NULL && DllName中沒有包含路徑)
{
調用LdrpCheckForKnownDll(...)函數,檢查該Dll是否是一個Known Dll,
如果是則調用NtOpenSection返回Dll的Section Handle,并跳到第(5)步。
}
LdrpKnownDllObjectDirectory和LdrpKnownDllPath在LdrpInitializeProcess中的第2步得到。
(2) 調用LdrpResolveDllName(...)函數,得到Dll的FullPathName和BaseDllName。
(3) 調用RtlDosPathNameToNtPathName_U(...)函數,將Dos pathname轉換成NT style pathname。
(4) 調用LdrpCreateDllSection(...)函數,得到Dll的Section Handle。
(5) 調用NtMapViewOfSection(...)函數,將Dll映射到進程的地址空間。
(6) 調用LdrpAllocateDataTableEntry(...)函數,分配一個loader data table entry。
Entry = LdrpAllocateDataTableEntry(ViewBase);
并初始化Entry中各項:
......
Entry->EntryPoint = LdrpFetchAddressOfEntryPoint(Entry->DllBase); // 得到Dll的入口點
(7) 調用LdrpInsertMemoryTableEntry(...)函數,將該entry加入到list of loaded modules for this process
在LdrpInitializeProcess中的第4步已經詳細介紹了LdrpInsertMemoryTableEntry()所做的工作。
(8) 剩下的大部分代碼與基址重定位有關,將之略去。

六、FreeLibrary

下面來學習動態卸載Dll(FreeLibrary)的代碼。FreeLibrary會導致調用LdrUnloadDll函數,相比較LdrLoadDll,它要簡單得多。

LdrUnloadDll:

    (1) 如果進程正在關閉中,立即返回。
(2) 調用LdrpCheckForLoadedDllHandle(...),判斷Dll是否存在,如果存在則返回它的LdrDataTableEntry。
(3) if (LdrDataTableEntry->LoadCount != -1)
{
LdrDataTableEntry->LoadCount--;
if (module是Image Dll)
調用LdrpUpdateLoadCount(...)函數,減少它所引用的Dll的LoadCount。
}
else
{
LoadCount等于-1說明這是進程靜態鏈接的Dll,直接退出。
}
(4) 初始化雙向鏈表LdrpUnloadHead。LdrpUnloadHead是個全局變量。
InitializeListHead(&LdrpUnloadHead);
(5) 沿著初始化方向的反方向建立unload list
while (從后向前遍歷InInitializationOrderModuleList表,找到每一個LDR_DATA_TABLE_ENTRY)
{
if (LoadCount == 0) // 引用計數為0表明該dll可以被卸載
{
RemoveEntryList(&Entry->InInitializationOrderLinks); // 從InInitializationOrderModuleList表中刪除
RemoveEntryList(&Entry->InMemoryOrderLinks); // 從InMemoryOrderList表中刪除
RemoveEntryList(&Entry->HashLinks); // 從Hash表中刪除

InsertTailList(&LdrpUnloadHead,&Entry->HashLinks); // 將該entry插入到LdrpUnloadHead表的末尾
}
}
(6) 初始化局部的unload list。
InitializeListHead(&LocalUnloadHead);
(7) while (從前向后遍歷LdrpUnloadHead鏈表中的每一項,找到每一個LDR_DATA_TABLE_ENTRY)
{
Entry->InMemoryOrderLinks.Flink = NULL; // 這是個標志,標志dll正在被unload

將dll從global unload list中移走,移入到local unload list中
RemoveEntryList(&Entry->HashLinks);
InsertTailList(&LocalUnloadHead,&Entry->HashLinks);

if (EntryPoint不為NULL && 設置了LDRP_PROCESS_ATTACH_CALLED標志(即the dll has been initialized))
{
調用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函數,執行EntryPoint函數。
}

RemoveEntryList(&Entry->InLoadOrderLinks); // 將其從InLoadOrderList表中刪除
}
(8) while (從前向后遍歷LocalUnloadHead鏈表中的每一項,找到每一個LDR_DATA_TABLE_ENTRY)
{
調用NtUnmapViewOfSection(...)函數,unmap在進程空間的映像。

執行一些其他的釋放工作。

RtlFreeHeap(Peb->ProcessHeap, 0,Entry); // 釋放LDR_DATA_TABLE_ENTRY所占用的內存。
}

LdrUnloadDll 里還有一些代碼是用于處理在EntryPoint函數里又執行了FreeLibrary的情況,這里沒有列出來,因為它會把邏輯搞得更復雜。不過不要誤以 為這些代碼無足輕重,事實上它們相當重要,在后面會講到,它們增強了FreeLibrary的安全性。

LdrUnloadDll看上去很簡單,但它還是留給了我一些疑惑:
疑惑一:InLoadOrderList和其他List不太一樣,是在執行完EntryPoint函數之后才將dll從InLoadOrderList中刪除的。可能是考慮到在EntryPoint函數里可能會執行一些需要用到InLoadOrderList的函數?
疑惑二:為什么要用兩個unload list,為什么要將global unload list拷到local unload list?代碼中的注釋說這是因為在執行init routine中,global list可能會改變。但這又有什么影響呢?


至此我們已經研究完了有關進程初始化、進程退出、Dll動態裝載、Dll動態卸載的代碼。現在我們可以根據學到的知識解決一些困惑已久的問題:

問題一

為什么要維護三個雙向鏈表:InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList?為什么Dll初始化順序不同于裝載的順序?
以Russ Osterlund的"Windows 2000 Loader"中帶的例子Test為例,下面是從ModuleList中截取的的部分輸出:

Ldr.InLoadOrderModuleList: 00131EC0 . 00134590
NO. Module Flags
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED

Ldr.InMemoryOrderModuleList: 00131EC8 . 00134598
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED

Ldr.InInitializationOrderModuleList: 00131F40 . 001345A0
1 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
3 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
6 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED

InLoadOrderModuleList的順序是從頭往后走,沿著flink的方向走,靠近頭部的是先load的。
InMemoryOrderModuleList的順序和LoadOrder相同。
InInitializationOrderModuleList的順序也是從頭往后走,沿著flink的方向走,靠近頭部的先初始化。
Unload的時侯按與初始化方向相反的方向,沿著blink的方向走,尾部的先unload。
(通 過查看win2k\private\windows\base\client\toolhelp.c以及win2k\private\ntos\dll\ ldrapi.c里LdrQueryProcessModuleInformation的代碼,可以知道通過toolhelp函數 Module32First,Module32Next得到的Module的順序是LoadOrder的順序。)

InMemoryOrderModuleList和InLoadOrderModuleList幾乎完全一樣,它唯一的特殊之處是在LdrUnloadDll里,通過
??????Entry->InMemoryOrderLinks.Flink = NULL;
標志dll正在被unload。在LdrpCheckForLoadedDll和LdrpCheckForLoadedDllHandle兩個函數里會用到這個特性。但這似乎不足以成為InMemoryOrderModuleList存在的理由?這仍是我的疑惑。

如果一個Dll A引用了另一個Dll B,那么就會出現Load的順序與Initialize的順序不一致的情況。
因為只有先load Dll A才可能知道它引用了Dll B,所以Dll A在InLoadOrderModuleList表中的順序顯示要先于Dll B。
又因為在邏輯上只有先知道Dll B能否初始化成功,才能決定Dll A是否能初始化成功,所以Dll B在InInitializationOrderModuleList表中的順序要先于Dll A。

問題二

在Russ Osterlund的"Windows 2000 Loader"的最后留下了一個問題:why do some DLLs have a reference count of -1 and the others contain an actual count?作者說以后會解答這個問題,我也不知道他后來在哪里解答了。可以把這個問題分為兩個小問題:哪些DLLs的引用計數為-1?為什么這些DLLs的引用計數要為-1?

在進程初始化的最開始,只有Process Image和ntdll.dll的LoadCount等于-1。
在裝載完static link dlls之后,象kernel32.dll之類的dll的引用計數都不等于-1,從LdrSnap的輸出可看出:

    LDR: Refcount   KERNEL32.dll (1)
LDR: Refcount USER32.dll (1)
LDR: Refcount KERNEL32.DLL (2)
LDR: Refcount GDI32.DLL (1)
LDR: Refcount KERNEL32.DLL (3)
LDR: Refcount USER32.DLL (2)

但 是隨后,初始化代碼把這些靜態鏈接的Dll的LoadCount都強制設為了-1。并不是說靜態鏈接的dll都要這么做,如果一個dll是通過 LoadLibrary動態加載的,那么它靜態鏈接的dll并不會強制設LoadCount為-1,下面是從ModuleList中截取的的部分輸出:

    LoadCount    Module                           Flag
1 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
2 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
1 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED

IMM32.dll是動態加載的,IMM32.dll靜態鏈接于ADVAPI32.DLL,ADVAPI32.DLL又靜態鏈接于RPCRT4.DLL,它們的引用計數都不等于-1。

通過LdrpInitializeProcess的偽代碼可以看出,所有并且只有Process Image靜態鏈接的Dlls的LoadCount為-1。
在 正常情況下,即LoadLibrary和FreeLibrary成對匹配的情況下,進程隱式鏈接的Dlls的引用計數永遠應該>=1,因為至少 Process Image在使用它。把它們的LoadCount設為-1,既是一種簡化的設計,也是一種安全的設計,因為即使是多次調用FreeLibrary也不會把 它釋放掉。LdrUnloadDll發現LoadCount等于-1,就立刻返回了。

問題三

為什么在DllMain里不能調用LoadLibrary和FreeLibrary函數?

MSDN里對這個問題的答案十分的晦澀。不過現在我們已經有了足夠的知識來解答這個問題。
考慮下面的情況:
??????(a)DllB靜態鏈接DllA
??????(b)DllB在DllMain里調用DllA的一個函數A1()
??????(c)DllA在DllMain里調用LoadLibrary("DllB.dll")

分 析:當執行到DllA中的DllMain的時侯,DllA.dll已經被映射到進程地址空間中,已經加入到了module list中。當它調用LoadLibrary("DllB.dll")時,首先會調用LdrpMapDll把DllB.dll映射到進程地址空間,并加入 到InLoadOrderModuleList中。然后會調用LdrpLoadImportModule(...)加載它引用的DllA.dll,而 LdrpLoadImportModule會調用LdrpCheckForLoadedDll檢查是否DllA.dll已經被加載。 LdrpCheckForLoadedDll會在哈希表LdrpHashTable中查找DllA.dll,而顯然它能找到,所以加載DllA.dll這 一步被成功調過。DllA在它的DllMain函數里能成功加載DllB,并要執行DllB的DllMain函數對其初始化。站在DllB的角度考慮,當 程序運行到它的DllMain的時侯,它完全有理由相信它隱式鏈接的DllA.dll已經被加載并且成功地初始化。可事實上,此時DllA只是處在"正在 初始化"的過程中!這種理想和現實的差距就是可能產生的Bug的根源,就是禁止在DllMain里調用LoadLibrary的理由!

本文附帶的例子中說明了這種出錯的情況:

TestLoad主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
FreeLibrary( hDll ) ;
return 0;
}

DllA:
HANDLE g_hDllB = NULL ;
char *g_buf = NULL ;

BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize begin!\n" ) ;

g_hDllB = LoadLibrary( "DllB.dll" ) ;

// g_buf在Load DllB.dll之后才初始化,顯然它沒有料到DllB在初始化時居然會用到g_buf!!
g_buf = newchar[128] ;
memset( g_buf, 0, 128 ) ;

OutputDebugString( "==>DllA: Initialize end!\n" ) ;
break ;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
returnTRUE;
}

DLLA_API void A1( char *str )
{
OutputDebugString( "==>DllA: A1()\n" ) ;

// 當DllB.dll在它的DllMain函數里調用A1()時,g_buf還沒有初始化,所以必然會出錯!
strcat( g_buf, "==>DllA: " ) ;
strcpy( g_buf, str ) ;

OutputDebugString( g_buf ) ;
}

DllB:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllB: Initialize!\n" ) ;
OutputDebugString( "==>DllB: DllB depend on DllA.\n" ) ;
OutputDebugString( "==>DllB: I think DllA has been initialize.\n" ) ;

// 當程序運行到這時,DllB認為它引用的DllA.dll已經加載并初始化了,所以它調用DllA的函數A1()
A1( "DllB Invoke DllA::A1()\n" ) ;
break ;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
returnTRUE;
}

在調用DllA的函數A1()時,因為DllA里有些變量還沒初始化,所以會產生exception。以下是截取的部分LDR的輸出,"==>"開頭的是程序的輸出。

    LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllA.dll
LDR: KERNEL32.dll used by DllA.dll
LDR: Snapping imports for DllA.dll from KERNEL32.dll
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllA.dll init routine 10001440
LDR: DllA.dll loaded. - Calling init routine at 10001440
==>DllA: Initialize begin!
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllB.dll
LDR: DllA.dll used by DllB.dll
LDR: Snapping imports for DllB.dll from DllA.dll
LDR: Refcount DllA.dll (2)
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllB.dll init routine 371260
LDR: DllB.dll loaded. - Calling init routine at 371260
==>DllB: Initialize!
==>DllB: DllB depend on DllA.
==>DllB: I think DllA has been initialize.
==>DllA: A1()
First-chance exception in Test.exe (DLLA.DLL): 0xC0000005: Access Violation.
==>DllA: Initialize end!

在 前面已經說過LdrUnloadDll里對DllMain里調用FreeLibrary的情況進行了特殊處理。此時仍然會對各個相關的Dll引用計數減 1,并移入到unload list中,但然后LdrUnloadDll就返回了!并沒有執行Dll的termination code。我構建了一個運行正確的例子TestUnload,說明LdrUnloadDll是怎么處理的。

考慮下面的情況:
??????(a)DllA依賴于DllC,DllB也依賴于DllC
??????(b)DllA里調用LoadLibrary("DllB.dll"),并保證其成功
??????(c)DllA在DllMain的termination code里執行FreeLibrary(),釋放DllB
??????(d)在主程序里動態的加載DllA

下面的代碼和注釋說明了程序運行的細節:

TestUnload主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
// 在調用LoadLibrary之后
// LoadOrderList: A(1) --> C(2) --> B(1), 括號內的代表LoadCount
// MemoryOrderList: A(1) --> C(2) --> B(1)
// InitOrderList: C(2) --> A(1) --> B(1)

FreeLibrary( hDll ) ;
return 0;
}

DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;

// 這里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
returnFALSE ;
break ;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;

case DLL_PROCESS_DETACH:
// 運行到這里時,DllA現在只留在LoadOrderList中,已經從另兩個list中刪除
// LoadOrderList: A(0) --> C(1) --> B(1)
// MemoryOrderList: C(1) --> B(1)
// InitOrderList: C(1) --> B(1)

OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;

FreeLibrary( g_hDllB ) ;

// 運行到這里時,DllB和DllC都從MemoryOrderList和InitOrderList中刪除了
// LoadOrderList: A(0) --> C(0) --> B(0)
// MemoryOrderList:
// InitOrderList:

OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
break;
}
returnTRUE;
}

如果主程序是靜態鏈接DllA又如何呢?LdrUnloadDll同樣能判斷這種情況:如果進程正在關閉那么LdrUnloadDll直接返回。我也構建了一個運行正確的例子TestUnload2來說明這種情況:

TestUnload2主程序:
int main(int argc, char* argv[])
{
// 此時DllA,DllB,DllC均已load
// LoadOrderList: A(-1) --> C(-1) --> B(1), 括號內的代表LoadCount
// MemoryOrderList: A(-1) --> C(-1) --> B(1)
// InitOrderList: C(-1) --> A(-1) --> B(1)

return 0;
}

DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;

// 這里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
returnFALSE ;

break ;

case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;

case DLL_PROCESS_DETACH:
// 運行到這里時,DllB已經被卸載,因為它是InitOrderList中最后一項
// 這里的卸載指的是調用了Init routine,發出了DLL_PROCESS_DETACH通知,而不是指unmap內存中的映像
OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;

// 這里不應該再調用DllB的函數!!!

// 盡管DllB已經被卸載,但這里調用FreeLibrary并無危險
// 因為LdrUnloadDll判斷出進程正在Shutdown,所以它什么也沒做,直接返回
FreeLibrary( g_hDllB ) ;

OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;

break;
}
returnTRUE;
}

在Jeffrey Richter的"Windows核心編程"和Matt Pietrek在1999年MSJ上的"Under theHood"里都說到,User32.dll在它的initializecode里會用LoadLibrary加載 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_DLLs" 下的dll,在它的terminate code里會用FreeLibrary卸載它們。跟蹤它的FreeLibrary函數,發現同上面的例子一樣,LdrUnloadDll發現進程正在 Shutdown中,就直接返回了,沒有任何危險。(User32.dll是靜態鏈接的函數,只可能在進程關閉時被卸載。另外,在我調試的時侯,發現即使 AppInit_DLLs下為空,User32.dll仍然會加載imm32.dll)。

總而言之,FreeLibrary本身是相當安全的,但MSDN里對它的警告也并非是胡說八道。在DllMain里使用FreeLibrary仍然是具有危險性的,與LoadLibrary一樣,它們具有相同的Bug哲學,即理想和現實的差距!
TestUnload2雖然運行正確,但是它具有潛在的危險性
對DllA而言,釋放DllB是它的責任,是它在收到DLL_PROCESS_DETACH通知之后用FreeLibrary卸載的,可事實上如果DllA被主程序靜態鏈接,或者DllA是動態鏈接但沒有用FreeLibrary顯式卸載它的話,那么在進程結束時,在DllA卸載DllB之前,DllB就已經被主程序卸載掉了!這種認識上的錯誤就是養育Bug的沃土。如果DllA沒有認識到這種可能性,而在FreeLibrary之前調用DllB的函數,就極可能出錯!!!

為了加深理解,我用文章開頭提到的那個Bug來說明這種情況,那可是血的教訓。問題描述如下:
我 用MFC寫了一個OCX,OCX里動態加載了一些Plugin Dlls,在OCX的ExitInstance(相當于DllMain里處理DLL_PROCESS_DETACH通知)里調用這些Plugin的 Uninitialize code,然后用FreeLibrary將其釋放。在我用MFC編寫的一個Doc/View架構的測試程序里運行良好,但不久客戶就報告了一個Bug:用 VB寫了一個OCX2來包裝我的OCX,在一個網頁里使用OCX2,然后在IE里打開這個網頁,在關掉IE時會當掉!發生在特定條件下的奇怪的錯誤!當時我可是費了不少功夫來解這個Bug,現在一切都那么清晰了。

下面是我用MFC寫的測試程序在關閉時的堆棧:

PDFREA_1!CPDFReaderOCXApp::ExitInstance+0x1d
PDFREA_1!DllMain+0x1bb
PDFREA_1!_DllMainCRTStartup+0x80
ntdll!LdrpCallInitRoutine+0x14
ntdll!LdrUnloadDll+0x29a
KERNEL32!FreeLibrary+0x3b
ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2b
ole32!CClassCache::CFinishComposite::Finish+0x19
ole32!CClassCache::FreeUnused+0x192
ole32!CoFreeUnusedLibraries+0x35
MFCO42D!AfxOleTerm+0x7b
MFCO42D!AfxOleTermOrFreeLib+0x12
MFC42D!AfxWinTerm+0xa9
MFC42D!AfxWinMain+0x103
ReaderContainerMFC!WinMain+0x18
ReaderContainerMFC!WinMainCRTStartup+0x1b3
KERNEL32!BaseProcessStart+0x3d

可以看到OCX被FreeLibrary顯式地釋放,搶在Plugin被進程釋放之前,所以不會出錯。

下面是關閉IE時的堆棧:

CPDFReaderOCXApp::ExitInstance() line 44
DllMain(HINSTANCE__ * 0x04e10000, unsigned long 0, void * 0x00000001) line 139
_DllMainCRTStartup(void * 0x04e10000, unsigned long 0, void * 0x00000001) line 273 + 17 bytes
NTDLL! LdrShutdownProcess + 238 bytes
KERNEL32! ExitProcess + 85 bytes

可 以看到OCX是在LdrShutdownProcess里被釋放的,而此時Plugin已經被釋放掉了,因為在 InInitializationOrderModuleList表里Plugin Dlls在OCX之后,所以它們被先釋放!這種情況要是還不出錯真是奇跡了。

總結:雖然MS警告不要在DllMain里不 能調用LoadLibrary和FreeLibrary函數,可實際上它還是做了很多的工作來處理這種情況。只不過因為他不想或者懶得說清楚到底哪些情況 不能這么用,才干脆一棒子打死統統不許。在你自己的程序里不是絕對不能這么用,只是你必須清楚地知道每件事是怎么發生的,以及潛在的危險。

后記:

這篇文章包含了太多的內容,你一定已經看得一頭霧水,不知我所云。不僅是你連我自己都有點吃不消。
我不是一個優秀的寫者,也無意于此。而且我一直認為,真正的知識永遠不是從書本上獲得的。
我不知道你能從這篇文章里學到什么,但你一定能從中知道你可以學到什么。

參考資料:

(1) Russ Osterlund, Windows 2000 Loader, MSDN Magazine, March 2002
(2) Matt Pietrek, Under the Hood, MSJ, September 1999
(3) Matt Pietrek, Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2, MSDN Magazine, March 2002
(4) Microsoft Portable Executable and Common Object File Format Specification, Revision 6.0 - February 1999
(5) Windows 2000 source code

下載

測試程序(包括TestLoad,TestUnload,TestUnload2) 下載后將擴展名.html改為.rar