2、DLL的入口点函数 一个DLL可以有一个入口点函数。系统会在不同的时候调用这个入口点函数,具体什么时候我们马上就会介绍。这些调用是通知性质的,通常被DLL用来执行一些与进程或线程有关的初始化和清理工作。如果DLL不需要这些通知,那么我们可以不必在源代码中实现这个入口点函数。例如:如果要创建一个只包含资源的DLL,那么我们就不需要实现这个函数。如果想要在DLL中接收通知,那么我们可以用switch语句来实现入口点函数。BOOL WINAPI DllMain(HINSTANCE hInstDll,DWORD fdwReason,PVOID fImpLoad); 说明:DllMain是区分大小写的。许多开发人员不小心将这个函数拼写为DLLMain。这是一个非常容易犯的错误,因为术语DLL经常会用全部大写字母表示。如果我们将入口点函数命名为DllMain之外的其它名称,那么虽然代码仍然能够编译和链接,但我们的入口点函数将永远不会被调用,DLL也永远不会进行初始化。 参数hInstDll包含该DLL实例的句柄,与WinMain的hInstExe参数相似,这个值表示一个虚拟内存地址,DLL的文件映像就被映射到进程地址空间中的这个位置。我们通常将这个参数保存在一个全局变量中,这样在调用资源载入函数的时候,我们就可以使用它。如果DLL是隐式载入的,那么最后一个参数fImpLoad的值将不为0,如果DLL是显式载入的,那么fImpLoad的值将为0。 参数fdwReason表示系统调用入口函数的原因。这个参数可能是下列4个值之一:DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH。下面几节就对他们进行介绍。 说明:必须记住,DLL使用DllMain函数来对自己进行初始化,DllMain函数执行的时候,同一个地址空间中的其它DLL可能还没有执行他们的 DllMain。这意味着它们尚未初始化,因此我们应该避免调用那些从其它DLL中导入的函数。此外,我们应该避免在DllMain中调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能会产生循环依赖。 2.1、DLL_PROCESS_ATTACH通知 当系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数,并在fdwReason参数中传入DLL_PROCESS_ATTACH。只有当DLL的映像文件第一次被映射的时候,才会这样。如果之后同一个线程再调用LoadLibrary(Ex)来载入一个已经被映射到进程的地址空间中的DLL,那么操作系统只不过是递增该DLL的使用计数,而不会再次用DLL_PROCESS_ATTACH来调用DllMain函数。 当一个DLL在处理DLL_PROCESS_ATTACH的时候,应该根据包含在DLL中的函数的需要,来执行与进程相关的初始化。举个例子,这个DLL中可能包含一些函数,这些函数需要使用自己的堆(在进程的地址空间中创建)。该DLL的DllMain函数可以在处理DLL_PROCESS_ATTACH通知的时候调用HeapCreate来创建所需的堆。为了让DLL中的函数能够访问刚才创建的堆的句柄,我们可以将它保存在一个该DLL函数能够访问到的全局变量中。 当DllMain处理DLL_PROCESS_ATTACH通知的时候,DllMain的返回值用来表示该DLL的初始化是否成功。例如:如果调用HeapCreate成功,那么DllMain应该返回true。如果无法创建堆,那么应该返回false。如果fdwReason是任何其它的值,那么系统将忽略DllMain的返回值。 当然,系统中的 某个线程 必须负责执行DllMain函数中的代码。创建新的进程时,系统会分配进程地址空间并将.exe的文件映像以及所需的DLL的文件映像 映射到 进程的地址空间中。然后,系统将创建进程的 主线程 并用这个主线程来调用每个DLL的DllM函数,同时传入DLL_PROCESS_ATTACH。当所有已映射的DLL都完成了对该通知的处理后,系统会先让进程的 主线程 开始执行可执行模块的C/C++运行时的启动代码,然后执行可执行模块的入口函数(WinMain或main)。如果任何一个DLL的DllMain函数返回false,也就是说初始化没有成功,那么系统会把所有的文件映像从地址空间中清除,向用户显示一个消息框来告诉用户进程无法启动,然后终止整个进程。 现在让我们来看一看显式载入DLL的时候会发生什么情况,进程调用LoadLibrary(Ex)的时候,系统会对指定的DLL进行定位,并将该DLL映射到进程的地址空间中。然后系统会 用 调用LoadLibrary(Ex)的线程来调用DLL的DllMain函数,并传入DLL_PROCESS_ATTACH值,当DLL的DllMain函数完成了对通知的处理后,系统会让LoadLibraryEx调用返回,这样线程就可以继续正常执行。如果DllMain函数返回false,也就是说初始化不成功,那么系统就会自动从进程的地址空间中撤销对DLL文件映像的映射,并让LoadLibraryEx返回NULL。 2.2、DLL_PROCESS_DETACH通知 当系统将一个DLL从进程的地址空间中撤销映射时,会调用DLL的DllMain函数,并在fdwReason参数中传入DLL_PROCESS_DETACH。当DLL处理这个通知的时候,应该执行与进程相关的清理工作。举个例子:DLL可能会调用HeapDistroy来销毁它在处理DLL_PROCESS_ATTACH通知的时候创建的堆。注意:如果DllMain函数在处理DLL_PROCESS_ATTACH的时候返回false,那么DllMain将不会收到DLL_PROCESS_DETACH通知。如果撤销映射的原因是因为进程要终止,那么调用ExitProcess函数的线程将负责执行DllMain函数的代码。在正常情况下,这个线程就是应用程序的主线程。当我们的入口点函数返回到C/C++运行时的启动代码后,启动代码会显式的调用ExitProcess来终止进程。 如果撤销的原因是因为进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread,那么发出调用的线程将执行DllMain函数中的代码。如果调用的是FreeLibrary,那么在处理完DLL_PROCESS_DETACH通知之前,线程是不会从该调用中返回的。 注意:DLL可能会阻止进程的终止。例如:当DllMain收到DLL_PROCESS_DETACH通知的时候,有可能会进入无限循环,只有当每个DLL都处理DLL_PROCESS_DETACH通知之后,操作系统才会真正的终止进程。 说明:如果进程终止是因为系统中的某个线程调用了TerminateProcess,系统便不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数,这意味着在进程终止之前,已映射到进程的地址空间中的任何DLL将没有任何机会执行任何清理代码。这可能会导致数据丢失。因此,除非万不得以,我们应该避免使用TerminateProcess函数。 2.3、DLL_THREAD_ATTACH通知 当进程创建一个线程的时候,系统会检查 当前映射到该进程的 地址空间中的 所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。这告诉DLL需要执行与线程相关的初始化。新创建的线程负责执行所有DLL的DllMain函数中的代码。只有当所有DLL都完成了对该通知的处理后,系统才会让新线程开始执行它的线程函数。 相反,如果系统将一个新的DLL映射到进程的地址空间中时,如果进程中已经有多个线程在运行,那么系统不会让任何已有的线程用DLL_THREAD_ATTACH来调用该DLL的DllMain函数。如果在创建新线程的时候,DLL已经被映射到进程的地址空间中,那么只有在这种情况下系统才会用DLL_THREAD_ATTACH来调用DLL的DllMain函数。 另外要注意的是,系统不会让进程的主线程用DLL_THREAD_ATTACH来调用DllMain函数。在进程创建的时候 被映射到进程地址空间中的任何DLL会收到DLL_PROCESS_ATTACH通知,但不会收到DLL_THREAD_ATTACH通知。 2.4、DLL_THREAD_DETACH 让线程终止的首选方式是让它的线程函数返回。这会使得系统调用ExitThread来终止线程。ExitThread告诉系统改线程想要终止,但系统不会理解终止线程,而会让这个即将终止的线程用DLL_THREAD_DETACH来调用所有已映射DLL的DllMain函数。这个通知告诉DLL执行与线程相关的清理。例如:C/C++运行库会在这个时候释放那些用来管理多线程应用程序的数据块。 注意:DLL可能会妨碍线程的终止。例如:当DllMain收到DLL_THREAD_DETACH通知的时候,有可能会进入无限循环。只有当每个DLL都处理完DLL_THREAD_DETACH通知之后,操作系统才会真正终止线程。 说明:如果线程终止是因为系统中的某个线程调用了TerminateThread,那么系统不会用DLL_THREAD_DETACH来调用所有DLL的DllMain函数。这意味着在线程终止之前,已映射到进程地址空间中的任何DLL将没有机会执行任何清理代码。这可能会导致数据丢失。因此,与TerminateProcess一样,除非万不得已,我们应该避免使用TerminateThread函数。 如果在撤销对一个DLL的映射时,还有任何线程在运行,那么系统不会让任何这些线程用DLL_THREAD_DETACH来调用DllMain。我们可能想要在自己处理DLL_PROCESS_DETACH的代码中对此进行检查,这样就能够进行任何必要的清理。 前面提到的这些规则可能会导致下面的情况:进程中的一个线程调用LoadLibrary来载入一个DLL,这使得系统用DLL_PROCESS_ATTACH来调用该DLL的DllMain函数(注意:该线程不会得到DLL_THREAD_ATTACH通知)。接着载入该DLL的线程退出,这使得系统再次调用DllMain函数----但这次传入的是DLL_THREAD_DETACH。注意:虽然系统将该线程链接到该DLL的时候,不会向该DLL发送DLL_THREAD_ATTACH通知,但是当系统将该线程与DLL解除链接的时候,却会向该DLL发送DLL_THREAD_DETACH通知。由于这个原因,我们在进行与线程相关的清理的时候必须极其小心。幸运的是,在大多数程序中,调用LoadLibrary的线程与调用FreeLibrary的线程是同一个线程。 2.5、DllMain的序列化调用 系统会将对DLL的DllMain函数的调用序列化。为了理解序列化的确切含义,让我们考虑下面的情形。一个进程有2个线程:线程A和线程B。进程的地址空间中还映射了另一个名为SomeDLL.dll的DLL。两个线程准备调用CreateThread函数来创建另外2个线程C和线程D。 当线程A调用CreateThread来创建线程C的时候,系统会用DLL_THREAD_ATTACH来调用SomeDLL.dll的DllMain函数。当线程C执行DllMain函数中的代码时,线程B用CreateThread来创建线程D。系统必须再次用DLL_THREAD_ATTACH来调用SomeDLL.dll的DllMain,但是这次是让线程D来执行其中的代码。但是,系统会将对DllMain的调用序列化,它会将线程D挂起,直到线程C执行完DllMain中的代码并返回为止。 当线程C执行完DllMain中的代码后,可以开始执行它的线程函数了。现在系统将唤醒线程D并让它执行DllMain中的代码。当函数返回后,线程D可以开始执行它的线程函数。 3、延迟载入DLL 为了让DLL更易于使用,Microsoft Visual C++提供了一项很棒的特性,即延迟载入DLL。一个延迟载入的DLL是隐式链接的,系统一开始不会将该DLL载入,只有当我们的代码试图去引用DLL中包含的一个符号时,系统才会实际载入DLL,延迟载入DLL在下列情况下非常有用。 1)如果应用程序使用了多个DLL,那么它的初始化可能会比较慢,因为 加载程序 要将所有必须的DLL映射到进程的地址空间中,缓解这个问题的一种方法是将DLL的载入过程延伸到进程的执行过程中。延迟载入DLL可以让我们很容易的实现这一点。 2)如何我们在代码中调用一个新的函数,然后又试图在一个不提供该函数的老版本的操作系统中运行该应用程序,那么加载程序会报告一个错误并且不允许应用程序运行。我们需要一种方法来让应用程序执行,如果(在运行的时候)发现应用程序正在老的操作系统下运行,那么就不调用这个不存在的函数。举个例子,假设应用程序想在Windows Vista下使用新的线程池函数,在老的系统下则使用老的函数。当应用程序初始化的时候,可以调用GetVersionEx来检查操作系统版本,并调用相应的函数。由于Windows Vista之前的Windows版本还没有这个函数,因此在老版本的Windows下运行这个应用程序会导致加载程序显示一条错误消息。延迟载入DLL同样可以让我们很容易的解决这个问题。 Microsoft非常好的实现了延迟载入特性,而且在所有版本的Windows上都能非常好的工作。但是它仍然存在一些局限性,值得在此一提,具体如下: 1)一个导出了字段(数据亦即全局变量)的DLL是无法延迟载入的 2)Kernel32.dll模块是无法延迟载入的,这是因为必须载入该模块才能调用Loadlibrary和GetProcAddress。 3)不应该在DllMain入口点函数中调用一个延迟载入的函数,因为这样可能会导致程序崩溃。 ............................................................. 4、函数转发器 函数转发器(function forwarder)是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的另一个函数。例如:如果用Visual C++的Dumpbin工具来查看Kernel32.dll那么我们会看到类似下面的输出。 c:/Windows/System32>dumpbin -exports Kernel32.dll
710 2C5 RtlFillMemory (forwarded to NTDLL.RtlFillMemory) 711 2C6 RtlMoveMemory (forwarded to NTDLL.RtlMoveMemory) 712 2C7 RtlUnwind (forwarded to NTDLL.RtlUnwind) 713 2C8 RtlZeroMemory (forwarded to NTDLL.RtlZeroMemory) 这个输出显示了4个被转发的函数,如果应用程序调用了RtlFillMemory,那么我们的可执行文件会被动的链接到Kernel32.dll。当可执行文件运行的时候,加载程序会载入Kernel32.dll并发现被转发的函数实际上是在NTDLL.dll中,然后他会将NTDLL.dll模块也一并载入。当可执行文件调用RtlFillMemory的时候,它实际上调用的是NTDLL.dll中的RtlFillMemory函数。 如果我们调用RtlFillMemory ,那么GerProcAddress会先在Kernel32.dll的导出段中查找,并发现RtlFillMemory是一个转发器函数,于是它会递归调用GetProcAddress,在NTDLL的导出段中查找RtlFillMemory 函数。 我们也可以在自己的DLL模块中使用函数转发器,最简单的方法是使用pragma指示符,如下所示: #pragma comment(linker,"/export:someFunc=DllWork.someOtherFunc") 这个pragma告诉链接器,正在编译的DLL应该导出一个名为someFunc的函数,但实际上实现someFunc的是另一个名为someOtherFunc的函数,该函数被包含在另一个DllWork.dll的模块中。我们必须为每个想要转发的函数单独创建一行pragma。 5、已知的DLL 系统对操作系统提供的某些DLL进行了特殊处理,这些DLL被称为已知的DLL(known DLL)。除了操作系统在载入它们的时候总是在同一个目录中查找之外,它们与其它的DLL并没有什么不同。在注册表中有这么一个注册表项: HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/KnownDLLs 我们可以看到,这个注册表项包含了一组值名,这些值名是一些DLL的名称。每个值名的数据正好等于值名加上.dll扩展名。(但这并不是必须的,稍候就会看到)。当LoadLibrary或LoadLibraryEx被调用的时候,函数首先会检查我们传入的DLL的名字是否包含了.dll扩展名。如果没有包含,那么函数会用正常的搜索顺序来搜索这个DLL。 如果我们指定了.dll扩展名,那么这两个函数会先将扩展名去掉,然后再在KnownDLLs注册表中搜索,看其中是否有与之相符的值名。如果没有值名与之相符,那么函数会使用正常的搜索规则。但是,如果找到了与之相符的值名,那么系统会查看与值名相对于的数据,并试图用该数据来载入DLL。系统还会从注册表项的DllDirectory值所表示的目录中开始搜索DLL。在Windows XP中DllDirectory的默认值为%SystemRoot%/system32 为了举例说明这一过程,假设我们在KnownDLLs注册表项中添加了下列值: value name:SomeLib value data:SomeOtherLib.dll 调用LoadLibrary("SomeLib");的时候,系统会用正常的搜索规则来对这个DLL进行定位. 但是,如果调用LoadLibrary("SomeLib.dll");的时候,系统会在knownDllsz注册表中发现有一个与之相符的名称。因此系统试图载入的DLL是SomeOtherLib.dll而不是SomeLib.dll。它首先会在%SystemRoot%/system32目录中查找SomeOtherLib.dll。如果在这个目录中找到了该文件,那么系统就会将它载入,如果系统未能在这个目录中找到该文件,那么LoadLibrary会失败并返回NULL,这时调用GetLastError将返回ERROR_FILE_NOT_FOUND。 6、DLL重定向 最初开发Windows的时候,内存和磁盘空间都非常宝贵,因此为了节约这些宝贵的资源,Windows的设计目标是尽可能的共享资源。出于这样的考虑,Microsoft建议将多个应用程序所共享的模块放在Windows的系统目录中,这使得系统能方便的定位和共享文件。C/C++运行库以及MFC就是很好的例子。 随着时间的推移,这成为了一个严重的问题,这是因为安装程序可能会用老版本的文件覆盖这个目录中的文件,或用不完全兼容的新版本的文件覆盖这个目录中的文件,从而妨碍用户的其它应用程序的正常运行。今天,硬盘不仅容量大而且价格便宜,内存也够用而且相对来说价格比以前要便宜很多。因此,Microsoft现在强烈建议开发人员将应用程序的文件放到自己的目录中,并且绝对不要碰Windows系统目录中的任何东西。这样既可以防止我们的应用程序妨碍其它应用程序,也可以避免其它应用程序妨碍我们的应用程序。 为了帮助开发人员,Microsoft从Windows 2000开始新增了一项DLL重定向特性。这项特性强制操作系统的加载程序 首先从应用程序的目录中载入模块。只有加载程序无法找到要找的文件时,才会在其它目录中搜索。 为了强制加载程序总是先检查应用程序的目录,我们所要做的就是将一个文件放到应用程序的目录中。这个文件的内容无关紧要,但它的文件名必须是AppName.local。举个例子,如果我们有一个名为SuperApp.exe的可执行文件,那么重定向文件的名称必须是SuperApp.exe.local。 LoadLibrary(Ex)在内部做了修改,来检查这个文件存在与否。如果应用程序的目录中存在这个文件,那么系统会载入这个目录中的模块。如果应用程序的目录中不存在这个文件,那么LoadLibrary的工作方式与以往相同。注意:除了创建一个.local文件,我们还可以创建一个名为.locald的文件夹。在这种情况下,我们可以将自己的DLL保存在这个文件夹中,让Windows能够轻易找到他们。 注意:为了安全性的缘故,Windows vista中这项特性在默认情况下是关闭的----因为它可能会使系统从应用程序的文件夹中载入伪造的系统DLL,而不是从Windows的系统文件夹中载入真正的系统DLL。为了打开这项特性,我们必须在HKLM/SoftWare/Microsoft/WindowsNT/CurrentVersion/Image File Execution Options注册表项中增加一个条目DWORD DevOverrideEnable,并将它的值设为1。 7、模块的基地址重定位 每个可执行文件 8、模块的绑定 |
|