"No-one normally bothers to understand what is that is does exactly, until strange or funny things start happening. "
DllMain我们都不会陌生,让我们先看看MS怎么说的 MSDN。
哦,仅仅如此么? 《Windows核心编程(第5版)》有比较稍微详细的介绍,我整理了下:
根据fdwReason参数,有四种情况DllMain函数会被调用。
1.DLL_PROCESS_ATTACH: 系统第一次将一个DLL映射到进程地址空间时。执行进程相关的初始化工作。
每个DLL的DllMain函数都是由创建进程的主线程调用的。进程初始化顺序是这样的:分配进程地址空间,映射exe及隐式dll文件映像到进程地址空间->主线程依次调用每个DLL的DllMain->主线程执行可执行代码块的crt startup code->执行_tmain或_tWinMain。
2.DLL_PROCESS_DETTACH: DLL从进程地址空间撤销时。执行进程相关的清理工作。
如果撤销映射的原因是因为进程要终止,那么调用ExitProcess的线程会执行DllMain。例如,当入口点函数返回到crt setup code时会显式调用ExitProcess终止进程。只有每个DLL都处理完DLL_PROCESS_DETTACH通知后,操作系统才会真正终止进程。
3.DLL_THREAD_ATTACH: 当进程创建新线程的时候,系统会调用当前映射到进程地址空间的每一个Dll文件映像的DllMain。执行线程相关的初始化工作。
当加载新dll时不会对已有线程调用DllMain函数。进程的主线程不会收到DLL_THREAD_ATTACH通知。
4.DLL_THREAD_DETACH: 当EixtThread被调用的时候,系统会让这个即将终止的线程用DLL_THREAD_DETACH调用所有已映射DLL的DllMain函数。进行线程相关的清理工作。
常见情况是,线程函数返回的时候系统调用EixtThread函数。当每个dll都处理完DLL_THREAD_DETACH的时候,操作系统才会真正的终止线程。撤销DLL的时候不会对已有线程调用DllMain函数。
目前为止,DllMain没有什么特殊的地方,看起来很单纯的一个函数,但是事实上呢.......
不知道你注意到没有,在MSDN里面有这样一句话——“There are serious limits on what you can do in a DLL entry point. To provide more complex initialization, create an initialization routine for the DLL. You can require applications to call the initialization routine before calling any other routines in the DLL.” 看起来有些玄机。
下面我们进入正题,谈谈什么事情不可以在DllMain里做。有很多事情我们都不能做,调用LoadLibrary/LoadLibraryEx是被严格禁止的。另外,不能调用User32.dll中的函数,不能使用CRT的内存管理(在/MD /MDd的时候),就是说不能调用malloc 。嗯,还不错...
这仅仅是个开始,更可怕的是当你做上面这些事的时候,不会得到任何编译器的错误甚至警告。甚至,根据不同环境,同一个文件可能表现出不同的行为,时而出现怪异的情况。当我们创建一个新的线程,当我们注册一个一个COM服务器,程序没报错就崩溃了,或者,停在DllMain不走了(deadlock)。
很不幸的,很多人遇到了上面的问题,解决问题的重点是:为什么?
DllMain调用的时机非常特殊,简而言之,当DLLMain调用时,操作系统的加载器处于很混乱的状态,我们使用的很多库都还没有初始化完成,我们的dll或exe文件还没加载完成。在调用DllMain前,系统还会请求一个锁。我们的DllMain在一系列静态依赖库的dll中的一个,因为每个静态依赖的库在加载的时候都会调用它的DllMain。事实上,各个库的加载顺序是不确定的,这取决于程序的依赖库以及特定的系统环境。
如果文件中的任何一个DllMain出了点错,那么我们的整个进程都完蛋了。
好吧,DllMain真是程序员的地狱。MS告诉我们要非常小心。
有些事是可以在DllMain里面做的:
1. 初始化静态和全局变量
2. 调用Kernel32.dll函数(除了LoadLibrary/LoadLibraryEx)
有些事是绝对不能做的:
1. 直接或者间接的动态dll绑定 LoadLibrary FreeLibrary
2. 任何形式的锁:很可能发生死锁
3. 跨库的调用:很可能调用的库还没被初始化(Kernel32除外)
4. 操作注册表
5. 创建线程/进程 以及 线程/进程 间通信
介于这些,我们应当合理的设计DllMain:
1. 最好的DllMain使用建议——不要用DllMain,逃离这个陷阱,我们得代码会变得更安全。额...,如果不用不行的话,那么我们就要非常小心了。
2. 使用DisableThreadLibraryCalls减少DllMain调用次数。
3. 使用2-phase initialization scheme 在DllMain中设置一个状态,让其他的函数做复杂的初始化过程。
参考资料:
1. 《Windows核心编程(第5版)》
2. MSDN
3. oleglv’s blog