Load and Unload

【peft】huggingface大模型加载多个LoRA并随时切换 peft加载多个lora模型并随时切换的方法 阅读详情

一、前言

在前一段时间,我遭遇了一个现象诡异的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_ENTRY和CONTAINING_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的机制。
*/

}
}
else if (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(...)
}
}
else if (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 = new char[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;
}
return TRUE;
}

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;
}
return TRUE;
}

在调用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)
return FALSE ;
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;
}
return TRUE;
}

如果主程序是静态链接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)
return FALSE ;

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;
}
return TRUE;
}

在Jeffrey Richter的"Windows核心编程"和Matt Pietrek在1999年MSJ上的"Under the Hood"里都说到, User32.dll在它的initialize code里会用LoadLibrary加载"HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/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
unload_dll:从正在运行的进程中卸载DLL。-开源 unload_dll是Windows的实用程序,可让您从正在运行的进程中卸载DLL。 如果要卸载的应用程序将自身挂接到其他进程中,但无法正确卸载,则此功能很有用。 立即下载

相关推荐

39文章解读与程序——EI\CSCD\北大核心《基于两阶段鲁棒优化算法的微网多电源容量配置》已提供下载资源

电网论文源程序擅长文章解读,论文与完整源程序,等方面的知识,电网论文源程序关注python,机器学习,计算机视觉,深度学习,神经网络,数据挖掘领域.电网论文源程序擅长文章解读,论文与完整源程序,等方面的知识,电网论文源程序关注python,机器学习,计算机视觉,深度学习,神经网络,数据挖掘领域.《《《《《《《《更多资源还请持续关注本专栏》》》》》》》《《《《《《《《更多资源还请持续关注本专栏》》》》》》》论文与完整源程序_电网论文源程序的博客-优快云博客。电网论文源程序-优快云博客。

LIANG674027206的博客 1209

LDR断链 隐藏DLL(转载)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。 转载链接:https://blog.youkuaiyun.com/yoie01/article/details/11696295 typedef struct _LSA_UNICODE_STRING { USHORT Length; USHORT MaximumL...

menglv_1978的专栏 1389

告别Nginx原始日志!VictoriaLogs+Grafana构建企业级应用日志分析平台实战

描述:做为一个合格的运维工程师,在面对企业安全运维管理中,日志审计是一项非常重要的工作。授人以鱼不如授人以渔,各位运友们可参照作者的方案设计,在自己的项目中根据实际情况调整落地,例如,不在仅仅采集 NGINX 应用日志,而是采集 Apache、Tomcat、SpringBoot 等多种应用服务器的日志,又如,不再仅仅采集网络设备、安全审计审计日志,而是 Windows、Linux 等多种操作系统的审计日志等,大家可充分发挥想象、实践能力,也欢迎大家在知识星球中分享自己的方案设计,一起交流学习。

全栈工程师修炼指南-IT知识分享 2480

从DllMain下断点到LdrpCallInitRoutine

windbg中有个sxe命令,用于启动某类事件上的调试中断。例如 sxe ld:kernel32.dll可以在exe加载kernel32.dll时中断到调试器。不过,一般情况下,exe无法捕获kernel32.dll加载的事件。因为当windbg启动捕获到ibp事件(初始断点 )而中断到调试器后,exe启动时所依赖的dll都已加载完毕(包括kernel32.dll)。如下面的清单,当windb

lixiangminghate的专栏 2098

QCM6490平台DDR测试:从QDUTT配置到眼图分析的实战指南

本文详细介绍了QCM6490平台DDR测试的全流程,从QDUTT工具安装配置到眼图分析,提供了实战操作指南和常见问题解决方案。通过具体案例分享,帮助工程师掌握DDR测试技巧,确保内存稳定性,特别适用于物联网和边缘计算设备的开发调试。

weixin_33910191的博客 459

MOOS-ivp实战:从零搭建水下机器人通信系统(Ubuntu 22.04版)

本文详细介绍了在Ubuntu 22.04系统上从零开始搭建MOOS-ivp水下机器人通信系统的完整实战流程。内容涵盖环境准备、核心安装、MOOSDB通信模型解析、自定义应用节点开发、以及调试监控工具链的使用,旨在帮助开发者快速掌握这一分布式机器人系统框架,构建稳定可靠的水下机器人通信与控制系统。

snow3的博客 636

Load and Unload - quoted from lostall

Load and Unloadlostall 一、前言在前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解释)。 MSDN里关于禁止在DllMain里调用LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍Russ Osterlund的"Windows 2000 Loader"一

明日帝国的专栏-技术改进生活 6460

Smart PLC高速脉冲输入计米轮模块(编码器测速/计米详细讲解)

线缆行业单绞机PLC控制算法详细解读可以参看下面的文章链接:线缆行业单绞机控制算法(详细图解+代码)_RXXW_Dor的博客-优快云博客在了解单绞机之前需要大家对收放卷以及排线控制有一定的了解,不清楚的可以参看下面几篇博客,这里不再赘述,受水平和能力所限,文中难免出现错误和不足之处,诚恳的欢迎大家批评和指正。PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-优快云博客_plc闭环控制实例。SMART PLC时间间隔定时器应用(高速脉冲测频/测速)_RXXW_Dor的博客-优快云博客。

RXXW_Dor的博客 2821

DllMain中不当操作导致死锁问题的分析--加载卸载DLL与DllMain死锁的关系

        前几篇文章一直没有在源码级证明:DllMain在收到DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH时会进入临界区。这个论证非常重要,因为它是使其他线程不能进入临界区从而导致死锁的关键。我构造了在DLL被映射到进程地址空间的场景,请看死锁时加载DLL的线程的堆栈(转载请指明出于breaksoftware的csdn博客)         如果仔细看过《...

方亮的专栏 5320

小波变换完全指南:从原理到实践的深度解析

在信号处理领域,傅里叶变换长期以来一直是频域分析的主要工具。然而,傅里叶变换存在一个根本性的局限:它只能告诉我们信号中包含哪些频率成分,却无法告诉我们这些频率成分在什么时刻出现。这就像是你知道一首歌用了哪些音符,却不知道这些音符的演奏顺序。小波变换(Wavelet Transform) 应运而生,它完美地解决了这个问题。小波变换不仅能够提供频率信息,还能同时提供时间信息,实现了信号的时频联合分析。本文将带你从零开始,深入理解小波变换的原理、实现和应用。傅里叶变换的定义如下: 它使用正弦和余弦这样的全局基函数

电脑管家 214

Android蓝牙中的Rfcomm协议介绍

RFCOMM(Radio Frequency Communications)是一种基于蓝牙技术的串口仿真协议,以下是详细介绍:一、协议概述二、工作原理三、应用场景

手机-手表蓝牙开发工程师的博客 714

C++ 专家编程(五)

原文:annas-archive.org/md5/57ea316395e58ce0beb229274ec493fc 译者:飞龙 协议:CC BY-NC-SA 4.0 第十五章:调试多线程代码 理想情况下,一个人的代码第一次就能正常工作,并且不包含等待崩溃应用程序、损坏数据或引起其他问题的隐藏错误。现实情况当然是不可能的。因此,开发了一些工具,使得检查和调试多线程应用程序变得容易。 在本章中,我们将研究其中一些内容,包括常规调试器以及 Valgrind 套件的一些工具,特别是 Helgrind 和 DRD

龙哥盟 1189

uniapp - 解决真机调试报错 request:fail abort statusCode:-1 java.security.cert.CertPathValidatorException: Tr

uni-app,真机调试报错,app端平台,安卓Android,安卓App发布线上报错,打包后接口请求失效了,api接口报错,真机运行安卓app时接口不通且报错,request:failabortstatusCode:-1java.security.cert.CertPathValidatorException:Trustanchorforcertificationpathnotfound.,真机调试,苹果ios真机报错,Uniapp打包到app后报错,uni-app真机调试报错,服务器

🏆 前端领域优质创作者 1万+

UE5 GameFeature Runtime Load and Unload

UE5 GameFeature Runtime Load or UnLoad

qq_52179126的博客 902

贝叶斯统计 [韦来生编]

贝叶斯(Bayes)统计是近几十年来迅速发展起来的数理统计的一个重要分支。贝叶斯方法与经典统计方法的主要不同之处是进行统计推断时除了利用样本信息外,还要利用参数的先验信息,因此可以提高统计推断或统计决策的效果。它在经济、金融、生物、医学、自然科学和社会科学等许多领域具有广泛的应用。贝叶斯方法的研究已渗透到了统计学的几乎所有领域。作者在给中国科学技术大学概率论与数理统计专业本科生开设的“数理统计”课程中有一章专门讲授“贝叶斯方法和统计决策理论”,近十年来也给中国科学技术大学概率统计专业研究生开设过几次“贝叶斯分析”课。本书是在对过去讲稿的内容作了适当的增减和调整的基础上完成的。  本书共分六章。第1章是绪论,介绍了贝叶斯统计的若干基本概念,同时对必要的数理统计的基础知识有重点地作了回顾。第二章介绍了确定先验分布的若干可供选择的方法。第三章介绍了几类常见统计模型参数的后验分布的主要结果和计算方法。第四章和第五章分别介绍了“贝叶斯统计推断”和“贝叶斯统计决策”的内容。第六章介绍了贝叶斯统计计算的若干方法,包括蒙特卡洛方法和马尔可夫链蒙特卡洛(MCMC)方法以及相关统计软件的简介。本书的每一章附右大量的例题和习题,书末附有常用的几个表格和部分习题解答供读者查用。  大约可在54学时内讲授本书的第1章至第六章的主要内容,教师可根据学时需要选讲本书第1章至第六章的部分内容。书中标“*”号的章节可略去不讲,留给读者作为阅读材料。第六章例题中的R代码和部分例题的数据文件等可从作者个人主页下载。  作者在编写本书的过程中参考了J.O.Berger教授的《统计决策论及贝叶斯分析》和茆诗松教授编写的《贝叶斯统计》等书中的一些内容,以及《贝叶斯分析》一书中由张伟平博士编写的有关贝叶斯统计计算的一些内容。在此表示衷心感谢。  本书编写准备过程中,中国科学技术大学统计与金融系的几个研究生帮助完成了本书部分中文的录入和排版,作者对他们的辛勤工作表示真诚的感谢。高等教育出版社为本书的出版给予了大力支持。在此一并致谢。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

haskell - load and unload modules

in haskell, you can dynamicall add or unload modules in the ghci interactive command prompt.    Load module Prelude&gt; import Data.Map Prelude Data.Map&gt;  or  Prelude&gt; :m +Data.Map Prelu...

iteye_13882的博客 378

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

NTDLL.dll4 : Load and Unload (ZT)

一、前言 在前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结 为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解释)。MSDN里关于禁止在DllMain里调用 LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍Russ Osterlund的"Windows 2000 Loader"一文,并仔细阅读了泄漏的Win20

it技术学习 1478

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

LKM -- load and unload process // My presentation

AUTHOR: Joseph Yang (杨红刚) CONTENT: LKM -- load and unload process NOTE: linux-3.0 LAST MODIFIED:09-27-2011 ----------------------------

ganggexiongqi的专栏 1864

[转]Using AppDomain to Load and Unload Dynamic Assemblies----work correctly

Recently a project I was working on required us to dynamically load an assembly at runtime, and invoke a method on the newly loaded assembly. After we had this working, I noticed that when we tried to...

weixin_34375233的博客 113

scull-scull.init , scripts as an alternative to scull_load and scull_unload

#!/bin/bash # Sample init script for the a driver module DEVICE="scull" SECTION="misc" # The list of filenames and minor numbers: $PREFIX

openCpu 959

Load and Unload /http://blog.donews.com/zwell/archive/2005/10/21/596302.aspx

 Load and Unloadlostall一、前言在前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解释)。MSDN里关于禁止在DllMain里调用LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍RussOsterlund的"Win

lookWang的专栏 1506

jquery.js 3.0报错, Uncaught TypeError: url.indexOf is not a function

转载自:http://majing.io/questions/432 问题描述 jQuery升级到3.0.0后类型错误 jquery.js:9612 Uncaught TypeError: url.indexOf is not a function 问题解决 这个错误是由于调用了load函数导致的错误 把代码里调用load函数的方式 $(wi...

weixin_34292287的博客 801

丝滑切换多个LoRA权重(LoRA加载与卸载方法)

对于 LoRA 方法特别说明:要合并并卸载当前激活的适配器,以便 LoRA 的权重被添加到基础模型的权重中,并移除注入的模型以恢复基础的 transformers 模型(同时保留添加的 LoRA 权重),请使用。

年少的勇气已经用完,剩下的就是三思而后行 2608
上一篇: 远线程删除自身(来自SWAN)
下一篇: 动态函数监控技术在缓冲区溢出检测中的应用
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值