Chinaunix首页 | 论坛 | 博客
  • 博客访问: 56735
  • 博文数量: 27
  • 博客积分: 2000
  • 博客等级: 大尉
  • 技术积分: 300
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-24 17:31
文章分类
文章存档

2011年(1)

2010年(8)

2009年(18)

我的朋友

分类: WINDOWS

2010-05-10 11:13:51

Load and Unload

一、前言

在前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解释)。
MSDN里关于禁止在DllMain里调用LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍Russ Osterlund的""一文,
并仔细阅读了泄漏的Win2000源代码的相关部分。按照我一贯的习惯,我的阅读过程形成了我这篇文章的主体。
自从我2000年写了""以来,我还没写过这么大块头的文章。
我不知道有多少人耐着性子看完了"ATL接口映射宏详解",我猜想这篇文章的命运也不会比它的前辈好多少。
在这个技术更新越来越快的年代里,人们会对这种陷入实现细节的文章感到厌烦,而我自己在若干年后可能也不会有耐心和勇气面对它,
但文章最后对几个问题的解释也还是有实用价值的,另外寻根究底的精神也总是应该存在的。

二、准备工作

工具

用SourceInsight看Win2000的源代码会比较爽。

WinDbg是调试用的神兵利器,它能显示比VC更多的调试信息,以及一些内部的数据结构,当然你需要先安装与你的OS相符合的调试符号。

GFlag.exe可以设置输出Loader Snap信息,它和WinDbg一起,都在Debugging Tools for Windows包里。

是我写的一个小工具,与本文相得益彰。



知识

在开始跟随我的脚步之前,你至少应该先阅读一下"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结构。(在中给出了这两个结构的定义)



浏览一遍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        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,参见中给出的定义。

    // 在进程堆上为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。这是一种类似的机制。
*/

}
}
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的安全性。

 
阅读(2334) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~