admin 管理员组

文章数量: 1086019


2024年3月21日发(作者:十六进制数转ascii码的方法)

Windows加载器与模块初始化

作者:Matt Pietrek

在最近的MSJ专栏(1999年六月)中,我讨论了COM类型库和数据库访问层,例如ActiveX®

数据对象(ADO)和OLE DB。MSJ专栏的长期读者可能认为我已经不行了(写不出技术层次比较

高的文章了)。为了重振雄风,这个月我要讲解一部分Windows NT®加载器代码,它是操作系统

和你的代码接合的地方。同时,我也会向你演示一些获取加载器状态信息的高超技巧,以及可以

用在Developer Studio®调试器中的相关技巧。

考虑一下你对EXE、DLL以及它们是如何被加载的和初始化的到底知道多少。你可能知道当

一个用C++写成的DLL被加载时,它的DllMain函数会被调用。想一想当你的EXE隐含链接到一

些DLL(例如,和)时到底发生了什么。这些DLL是以什么顺序被初

始化的?某个DLL将要被初始化,而它所依赖的其它DLL还未被初始化,这可能吗?Platform SDK

在“Dynamic Link Library Entry Point Function(动态链接库入口点函数)”一节中对此描述

如下:

“你的函数应该仅进行一些简单的初始化任务,例如设置线程局部存储(TLS),创建同步对

象和打开文件等。它绝对不能调用LoadLibrary函数,因为这可能在DLL加载顺序上造成循环依

赖。这可能导致即将使用一个DLL但是系统还未对它进行初始化。同样,你也不能在入口点函数

中调用FreeLibrary函数,因为这可能导致即将使用一个DLL但是系统已经执行完了它的终止代

码。”

“调用除TLS函数、同步函数和文件函数之外的Win32®函数也可能引起很难诊断的问题。

例如,调用User函数、Shell函数和COM函数可能引起访问违规,因为这些DLL中一些函数调

用LoadLibrary加载其它系统组件。”

看了上述文档后我的第一反应是它太含糊了。例如你想在自己的DllMain函数中读取注册表

是再正常不过的事了,它当然可以作为初始化的一部分。但不幸的是,在你的DllMain代码开始

执行时还没有初始化。这样,对注册表API的调用将会失败。

在上述文档中对使用LoadLibrary给出了严厉的警告。但非常有趣的是,Windows NT的

却明确地忽略前面的忠告。你可能知道Widnows NT上的一个注册表键AppInit_Dlls,

它用来加载一系列DLL到每个进程。事实表明,是USER32在初始化时加载这些DLL的。USER32

在它的DllMain代码中查看这个注册表键并调用LoadLibrary加载这些DLL。稍微思考一下就会

知道,如果你的应用程序不使用的话,AppInit_Dlls这个技巧就不能发挥作用。不

过,这有点跑题了。

我之所以要讲解这方面的内容是因为DLL的加载与初始化还是一片盲区。在大多数情况下,

对操作系统加载器是如何工作的有一个简单的印象就足够了。然而,在极少数情况下,除非你对

操作系统加载器的行为方式有比较详细的了解,否则就会陷入困境之中。

加载器醒来!

大多数程序员所认为的模块加载过程实际上分为两个截然不同的步骤。第一步是把EXE或

DLL映射进内存。此时加载器查看模块的导入地址表(IAT)来判断这个模块是否依赖于其它DLL。

如果它依赖的DLL还未被加载进那个进程,加载器也将它们映射进内存。这个过程递归进行,直

到所有依赖的模块都被映射进内存。要查看一个可执行文件隐含依赖的所有DLL,最好的方法是

使用Platform SDK附带的DEPENDS程序。

第二步是初始化所有DLL。在第一步中,当操作系统把EXE和DLL映射进内存时,它并不调

用相应的初始化例程。初始化例程是在所有模块都被映射进内存之后才被调用的。关键是:DLL

被映射进内存的顺序并不需要与它们被初始化的顺序一样。我曾经见到有人看到Developer

Studio调试器中对DLL映射时的通知而误认为DLL是以相同的顺序被初始化的。

在Windows NT中,调用EXE和DLL入口点代码的例程被称为LdrpRunInitializeRoutines。在

平常的工作中,我已经多次跟踪到LdrpRunIntializeRoutines的汇编代码中。但是,看着大堆的

汇编代码并不是理解它的好方法。因此,我用类似C++的伪代码重写了Windows NT 4.0 SP3 的

LdrpRunInitializeRoutines函数,如

图1所示。实际上,在中这个例程的名字按

__stdcall调用约定被粉碎成了_LdrpRunInitializeRoutines@4。在伪代码中,除了那些名字前

面加了下划线的,其余的都是我起的名字。

在Windows NT加载器代码中,LdrpRunInitializeRoutines是调用EXE或DLL的指定入口

点代码之前的最后一站。(在下面的讨论中,我将把 “入口点”和“初始化例程”互换着使用。)

这段加载器代码在被加载的DLL所在的那个进程环境中执行。也就是说,它并不是什么特别的加

载器进程的一部分。在进程启动过程中处理隐含加载的DLL时,LdrpRunInitializeRoutines至

少被调用一次。同时,每当动态加载一个或多个DLL(一般是通常调用LoadLibrary实现的)时,

都要调用它,

每当LdrpRunInitializeRoutines执行时,它就查找并调用已经被映射进内存但还未被初始

化的所有DLL的入口点代码。在看上面的伪代码时,注意所有提供跟踪输出的额外代码(也就是

上面的伪代码中使用_ShowSnaps变量和_DbgPrint函数的代码),它们甚至存在于非调试版的

Windows NT中。稍候我会接着说这一点。

这个函数大体上分为四个不同的部分。第一部分调用_LdrpClearLoadInProgress函数。这

个NTDLL函数返回刚才映射进内存的DLL的数目。例如,如果你在中调用LoadLibrary

函数,而FOO隐含链接到了和,那么_LdrpClearLoadInProgress将返回3,因

为有三个DLL被映射进内存中。

在知道了相关的DLL数目之后,LdrpRunInitializeRoutines调用_RtlAllocateHeap(也被

称为HeapAlloc)来为一个指针数组分配内存。在伪代码中我把这个数组称为pInitNodeArray。

这个数组中的每个元素(指针)最终分别指向一个包含有关最近加载(但尚未初始化)的DLL

的信息的结构。

在LdrpRunInitializeRoutines的第二部分中,它使用内部进程数据结构来获取一个包含最

近加载的DLL的链表。然后它遍历这个链表来确定加载器是否曾经加载过这个DLL。接下来确定

DLL是否有入口点函数。如果这两个测试都通过了,它就将指向相应模块信息的指针添加到

pInitNodeArray数组中。在伪代码中我称这个模块信息为pModuleLoaderInfo。一定要注意:一


本文标签: 加载 函数 代码 可能 调用