分享
 
 
 

WINDOWS 2K Dll 加载过程

王朝system·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

WINDOWS 2K Dll 加载过程

jefong by 2005/03/30

这片文章是我在阅读完MSJ September 1999 Under the Hood后的总结。

在windows中exe可执行程序运行时都会调用一些DLL,例如KERNEL32.DLL和USER32.DLL等系统的dll。但是dll是怎么被加载的呢?通常,大家都知道在编写dll时会有一个DLLMain的入口函数,但是实际上这个函数并不是调用dll时最先的工作。首先dll需要被加载,然后要进行初始化分配,再之后才进入DLLMain。还有可能你的一个dll中还会调用另一各dll。那么dll到底是怎样加载和初始化的呢,我们来参考一下Platform SDK中的“Dynamic-Link Library Entry-Point Function”。

你的函数正在执行一个初始化任务,例如设置TLS,创建同步对象或打开一个文件。那么你在函数中一定不要调用LoadLibrary函数,因为dll加载命令会创建一个依赖循环。这点会导致在系统执行dll的初始化代码前就已经调用了dll的函数。例如,你不能在入口函数中调用FreeLibrary函数,因为这样会使系统在已经结束了dll后还调用dll中的操作,引起严重错误。

初始化任务时调用Win32函数也会引起错误,例如调用User,Shell和COM函数可能会引起存储无效的错误,因为dll中一些函数会调用LoadLibrary来加载别的系统组件。

当你在你的DllMain函数中读一个注册表键值,这样做会被限制,因为在正常情况下ADVAPI32.DLL在你执行DllMain代码时还没被初始化,所以你调用的读注册表的函数会失败。

在文档中初始化部分使用LoadLibrary函数是严格限制的,但是存在特殊的情况,在WindowsNT中USER32.DLL是忽略上面的限制的。这样一来好像与上面所说的相背了,在USER32.DLL的初始化部分出现了调用LoadLibrary加载dll的部分,但是没有出现问题。这是因为AppInit_Dlls的原因,AppInit_Dlls可以为任一个进程调用一个dll列表。所以,如果你的USER32.dll调用出现问题,那一定是AppInit_Dlls没有工作。

接下来,我们来看看dll的加载和初始化是怎样完成的。操作系统有一个加载器,加载一个模块通常有两个步骤:1.把exe或dll映象到内存中,这时,加载器会检查模块的导入地址表(IAT),看模块是否依赖于附加的dll。如果dll还没有被加载到进程中,那么加载器就把dll映象到内存。直到所有的未加载的模块都被映象到内存。2.初始化所有的dll。在windows NT中,系统调用exe和dll入口函数的程序会先调用LdrpRunInitializeRoutines函数,也就是说当你调用LoadLibrary时会调用LdrpRunInitializeRoutines,当调用LdrpRunInitializeRoutines时会首先检查已经映射到内存的dll是否已经被初始化。我们来看下面的代码(Matt的LdrpRunInitializeRoutines伪代码):

//=============================================================================

// Matt Pietrek, September 1999 Microsoft Systems Journal

// 中文注释部分为jefong翻译

//

// Pseudocode for LdrpRunInitializeRoutines in NTDLL.DLL (NT 4, SP3)

//

// 当LdrpRunInitializeRoutines 在一个进程中第一次被调用时(这个进程的隐式链接模块已经被初始化),bImplicitLoad 参数是非零。当使用LoadLibrary调用dll时,bImplicitLoad 参数是零;

//=============================================================================

#include <ntexapi.h> // For HardError defines near the end

// Global symbols (name is accurate, and comes from NTDLL.DBG)

// _NtdllBaseTag

// _ShowSnaps

// _SaveSp

// _CurSp

// _LdrpInLdrInit

// _LdrpFatalHardErrorCount

// _LdrpImageHasTls

NTSTATUS

LdrpRunInitializeRoutines( DWORD bImplicitLoad )

{

// 第一部分,得到可能需要初始化的模块的数目。一些模块可能已经被初始化过了

unsigned nRoutinesToRun = _LdrpClearLoadInProgress();

if ( nRoutinesToRun )

{

// 如果有需要初始化的模块,为它们分配一个队列,用来装载各模块信息。

pInitNodeArray = _RtlAllocateHeap(GetProcessHeap(),

_NtdllBaseTag + 0x60000,

nRoutinesToRun * 4 );

if ( 0 == pInitNodeArray ) // Make sure allocation worked

return STATUS_NO_MEMORY;

}

else

pInitNodeArray = 0;

//第二部分;

//进程环境块(Peb),包含一个指向新加载模块的链接列表的指针。

pCurrNode = *(pCurrentPeb->ModuleLoaderInfoHead);

ModuleLoaderInfoHead = pCurrentPeb->ModuleLoaderInfoHead;

if ( _ShowSnaps )

{

_DbgPrint( "LDR: Real INIT LIST\n" );

}

nModulesInitedSoFar = 0;

if ( pCurrNode != ModuleLoaderInfoHead ) //判断是否有新加载的模块

{

while ( pCurrNode != ModuleLoaderInfoHead ) //遍历所有新加载的模块

{

ModuleLoaderInfo pModuleLoaderInfo;

//

//一个ModuleLoaderInfo结构节点的大小为0X10字节

pModuleLoaderInfo = &NextNode - 0x10;

localVar3C = pModuleLoaderInfo;

//

// 如果模块已经被初始化,就忽略

// X_LOADER_SAW_MODULE = 0x40 已被初始化

if ( !(pModuleLoaderInfo->Flags35 & X_LOADER_SAW_MODULE) )

{

//

// 模块没有被初始化,判断是否具有入口函数

//

if ( pModuleLoaderInfo->EntryPoint )

{

//

// 具有初始化函数,添加到模块列表中,等待进行初始化

pInitNodeArray[nModulesInitedSoFar] =pModuleLoaderInfo;

// 如果ShowSnaps为非零,那么打印出模块的路径和入口函数的地址

// 例如:

// C:\WINNT\system32\KERNEL32.dll init routine 77f01000

if ( _ShowSnaps )

{

_DbgPrint( "%wZ init routine %x\n",

&pModuleLoaderInfo->24,

pModuleLoaderInfo->EntryPoint );

}

nModulesInitedSoFar++;

}

}

// 设置模块的X_LOADER_SAW_MODULE标志。说明这个模块还没有被初始化。

pModuleLoaderInfo->Flags35 &= X_LOADER_SAW_MODULE;

// 处理下一个模块节点

pCurrNode = pCurrNode->pNext

}

}

else

{

pModuleLoaderInfo = localVar3C; // May not be initialized???

}

if ( 0 == pInitNodeArray )

return STATUS_SUCCESS;

// ************************* MSJ Layout! *****************

// If you're going to split this code across pages, this is a great

// spot to split the code. Just be sure to remove this comment

// ************************* MSJ Layout! *****************

//

// pInitNodeArray指针包含一个模块指针队列,这些模块还没有 DLL_PROCESS_ATTACH

// 第三部分,调用初始化部分

try // Wrap all this in a try block, in case the init routine faults

{

nModulesInitedSoFar = 0; // Start at array element 0

//

// 遍历模块队列

//

while ( nModulesInitedSoFar < nRoutinesToRun )

{

// 获得模块指针

pModuleLoaderInfo = pInitNodeArray[ nModulesInitedSoFar ];

// This doesn't seem to do anything...

localVar3C = pModuleLoaderInfo;

nModulesInitedSoFar++;

// 保存初始化程序入口指针

pfnInitRoutine = pModuleLoaderInfo->EntryPoint;

fBreakOnDllLoad = 0; // Default is to not break on load

// 调试用

// If this process is a debuggee, check to see if the loader

// should break into a debugger before calling the initialization.

//

// DebuggerPresent (offset 2 in PEB) is what IsDebuggerPresent()

// returns. IsDebuggerPresent is an NT only API.

//

if ( pCurrentPeb->DebuggerPresent || pCurrentPeb->1 )

{

LONG retCode;

//

// Query the "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft // Windows NT\CurrentVersion\Image File Execution Options"

// registry key. If a a subkey entry with the name of

// the executable exists, check for the BreakOnDllLoad value.

//

retCode =

_LdrQueryImageFileExecutionOptions(

pModuleLoaderInfo->pwszDllName,

"BreakOnDllLoad",pInitNodeArray

REG_DWORD,

&fBreakOnDllLoad,

sizeof(DWORD),

0 );

// If reg value not found (usually the case), then don't

// break on this DLL init

if ( retCode <= STATUS_SUCCESS )

fBreakOnDllLoad = 0;pInitNodeArray

}

if ( fBreakOnDllLoad )

{

if ( _ShowSnaps )

{

// Inform the debug output stream of the module name

// and the init routine address before actually breaking

// into the debugger

_DbgPrint( "LDR: %wZ loaded.",

&pModuleLoaderInfo->pModuleLoaderInfo );

_DbgPrint( "- About to call init routine at %lx\n",

pfnInitRoutine )

}

// Break into the debugger

_DbgBreakPoint(); // An INT 3, followed by a RET

}

else if ( _ShowSnaps && pfnInitRoutine )

{

// Inform the debug output stream of the module name

// and the init routine address before calling it

_DbgPrint( "LDR: %wZ loaded.",

pModuleLoaderInfo->pModuleLoaderInfo );

_DbgPrint("- Calling init routine at %lx\n", pfnInitRoutine);

}

if ( pfnInitRoutine )

{

// 设置DLL_PROCESS_ATTACH标志

//

// (Shouldn't this come *after* the actual call?)

//

// X_LOADER_CALLED_PROCESS_ATTACH = 0x8

pModuleLoaderInfo->Flags36 |= X_LOADER_CALLED_PROCESS_ATTACH;

//

// If there's Thread Local Storage (TLS) for this module,

// call the TLS init functions. *** NOTE *** This only

// occurs during the first time this code is called (when

// implicitly loaded DLLs are initialized). Dynamically

// loaded DLLs shouldn't use TLS declared vars, as per the

// SDK documentation

// 如果模块需要分配TLS,调用TLS初始化函数

// 注意只有在第一次调时(bImplicitLoad!=0)才会分配TLS,就是隐式dll加载时

// 当动态加载时(bImplicitLoad==0)就不需要声明TLS变量

if ( pModuleLoaderInfo->bHasTLS && bImplicitLoad )

{

_LdrpCallTlsInitializers( pModuleLoaderInfo->hModDLL,

DLL_PROCESS_ATTACH );

}

hModDLL = pModuleLoaderInfo->hModDLL

MOV ESI,ESP // Save off the ESP register into ESI

// 设置入口函数指针

MOV EDI,DWORD PTR [pfnInitRoutine]

// In C++ code, the following ASM would look like:

//

// initRetValue =

// pfnInitRoutine(hInstDLL,DLL_PROCESS_ATTACH,bImplicitLoad);

//

PUSH DWORD PTR [bImplicitLoad]

PUSH DLL_PROCESS_ATTACH

PUSH DWORD PTR [hModDLL]

CALL EDI // 调用入口函数

MOV BYTE PTR [initRetValue],AL // 保存入口函数返回值

MOV DWORD PTR [_SaveSp],ESI // Save stack values after the

MOV DWORD PTR [_CurSp],ESP // entry point code returns

MOV ESP,ESI // Restore ESP to value before the call

//

// 检查调用前后的ESP值是否一至

//

if ( _CurSP != _SavSP )

{

hardErrorParam = pModuleLoaderInfo->FullDllPath;

hardErrorRetCode =

_NtRaiseHardError(

STATUS_BAD_DLL_ENTRYPOINT | 0x10000000,

1, // Number of parameters

1, // UnicodeStringParametersMask,

&hardErrorParam,

OptionYesNo, // Let user decide

&hardErrorResponse );

if ( _LdrpInLdrInit )

_LdrpFatalHardErrorCount++;

if ( (hardErrorRetCode >= STATUS_SUCCESS)

&& (ResponseYes == hardErrorResponse) )

{

return STATUS_DLL_INIT_FAILED;

}

}

//

// 入口函数返回0,错误

//

if ( 0 == initRetValue )

{

DWORD hardErrorParam2;

DWORD hardErrorResponse2;

hardErrorParam2 = pModuleLoaderInfo->FullDllPath;

_NtRaiseHardError( STATUS_DLL_INIT_FAILED,

1, // Number of parameters

1, // UnicodeStringParametersMask

&hardErrorParam2,

OptionOk, // OK is only response

&hardErrorResponse2 );

if ( _LdrpInLdrInit )

_LdrpFatalHardErrorCount++;

return STATUS_DLL_INIT_FAILED;

}

}

}

//

// 如果EXE已经拥有了TLS,那么调用TLS初始化函数,也是在进程第一次初始化dll时

//

if ( _LdrpImageHasTls && bImplicitLoad )

{

_LdrpCallTlsInitializers( pCurrentPeb->ProcessImageBase,

DLL_PROCESS_ATTACH );

}

}

__finally

{

//

// 第四部分;

// 清除分配的内存

_RtlFreeHeap( GetProcessHeap(), 0, pInitNodeArray );

}

return STATUS_SUCCESS;

}

这个函数分为四个主要部分:

一:第一部分调用_LdrpClearLoadInProgress函数,这个NTDLL函数返回已经被映象到内存的dll的个数。例如,你的进程调用exm.dll,而exm.dll又调用exm1.dll和exm2.dll,那么_LdrpClearLoadInProgress会返回3。得到dll个数后,调用_RtlAllocateHeap,它会返回一个内存的队列指针。伪码中的队列指针为pInitNodeArray。队列中的每个节点指针都指向一个新加载的dll的结构信息。

二:第二部分的代码通过进程内部的数据结构获得一个新加载dll的链接列表。并且检查dll是否有入口指针,如果有,就把模块信息指针加入pInitNodeArray中。伪码中的模块信息指针为pModuleLoaderInfo。但是有的dll是资源文件,并不具有入口函数。所以pInitNodeArray中节点比_LdrpClearLoadInProgress返回的数目要少。

三:第三部分的代码枚举了pInitNodeArray中的对象,并且调用了入口函数。因为这部分的初始化代码有可能出现错误,所以使用了_try异常扑获功能。这就是为什么在DllMain中出现错误后不会使整个进程终止。

另外,在调用入口函数时还会对TLS进行初始化,当用 __declspec来声明TLS变量时,链接器包含的数据可以进行触发。在调用dll的入口函数时,LdrpRunInitializeRoutines函数会检查是否需要初始化一个TLS,如果需要,就调用_LdrpCallTlsInitializers。

在最后的伪代码部分使用汇编语言来进行dll的入口函数调用。主要的命令时CALL EDI;EDI中就是入口函数的指针。当此命令返回后,dll的初始化工作就完成了。对于C++写的dll,DllMain已经执行完成了它的DLL_PROCESS_ATTACH代码。注意一下入口函数的第三个参数pvReserved,当exe或dll隐式调用dll时这个参数是非零,当使用LoadLibrary调用时是零。在入口函数调用以后,加载器会检查调用入口函数前和后的ESP的值,如果不同,dll的初始化函数就会报错。检查完ESP后,还会检查入口函数的返回值,如果是零,说明初始化的时候出现了什么问题。并且系统会报错并停止调用dll。在第三部分的最后,在初始化完成后,如果exe进程已经拥有了TLS,并且隐式调用的dll已经被初始化,那么会调用_LdrpCallTlsInitializers。

四:第四部分代码是清理代码,象_RtlAllocateHeap 分配的pInitNodeArray的内存需要被释放。释放代码出现在_finally块中,调用了_RtlFreeHeap 。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有