转自:https://blog.csdn.net/qq_42021840/article/details/105956819
前几天的时候,遇到一个面试 ,问我在Dll中的忌讳有什么?我回答的是不能在dll main函数中创建线程和加载动态库,然后下一个问题也接着来了,为什么不能?我说会造成死锁,但是具体的是什么原因造成的死锁,当时只记得看过《Windows 核心编程》,书中有过介绍,但是忘记了,无非就是因为同步问题,线程间会造成互相阻塞状态。具体是什么,今天回来分析一下。
Windows 核心编程中的原话是这样说的:
DLL 必须使用DllMain函数来对自己进行初始化。DllMain函数执行的时候,同一个地址空间的中的其他DLL可能还没有初始化,也就是没有调用其他DLL 的DllMain函数,所以我们应该尽量避免去使用从其他DLL中导入的函数。此外,还应该避免在DllMain中调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能会产生循环依赖。
-
BOOL WINAPI DllMain(
-
_In_ HANDLE hInstance,
-
_In_ ULONG fdwReason,
-
LPVOID Reserved
-
)
//dll main 函数
-
{
-
printf(
"%p\r\n", hInstance);
-
switch (fdwReason)
-
{
-
case DLL_PROCESS_DETACH:
//0
-
{
-
break;
-
}
-
case DLL_PROCESS_ATTACH:
//1
-
{
-
break;
-
}
-
case DLL_THREAD_ATTACH:
//2
-
{
-
break;
-
}
-
case DLL_THREAD_DETACH:
//3
-
{
-
break;
-
}
-
}
-
return TRUE;
-
}
- hInstance:该DLL示例的句柄。这个值表示一个虚拟的地址,DLL的文件映像就储存在这个位置。
- fdwReason:表示调用入口点函数的原因/
- Reserved:如果DLL是隐式加载的,那么该值不为零,否则为0。
DLL_PROCESS_ATTACH 1
当系统第一次将一个DLL映射到进程的地址空间的时候,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_ATTACH。若在第一次映射之后,调用LoadLibrary来载入一个已经映射过的DLL后,操作系统只会递增该DLL的引用计数,并不会调用DllMain。
系统中的某个线程必须负责执行DllMian函数中的代码。创建新的线程的时候,系统会分配进程地址空间并将.exe文件的映像映射到进程的地址空间中。然后,系统将创建进程的主线程,并用这个主线程来调用每个DLL的DllMain函数,同时传入DLL_PROCESS_ATTACH。当所有的已经映射的DLL 都完成了DllMain的调用,那么系统就会让主线程取开始执行.exe的C/C++运行时的启动代码,然后执行.exe的入口点函数(main或 WinMain)。
DLL_PROCESS_DETACH 0
当系统将一个DLL从进程的地址空间中撤销映射时,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_DETACH。如果当DLL_PROCESS_ATTACH时,返回是False,那么将不会有DLL_PROCESS_DETACH的通知。
如果撤销映射的原因是因为进程要被终止,那么调用和ExitProcess函数的线程将负责执行DllMain函数。
如果撤销映射的原因是因为进程中的一个线程调用了FreeLibrary,那么发出调用的线程将执行DllMain函数中的代码。并在DllMian处理完DLL_PROCESS_DETACH通知之前,线程是不会返回的。
注意:
Dll可能会阻碍进程的终止。只有当每个Dll都处理完DLL_PROCESS_DETACH通知之后,操作系统才会终止进程。
如果进程终止是因为TerminateProcess,那么系统不会用DLL_PROCESS_DETACH来调用DllMian。
DLL_THREAD_ATTACH 2
当进程创建一个线程的时候,系统会检测当前映射到该进程地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。新建线程负责执行所有的DLL的DllMain函数中的代码。只有当所有DLL都完成了对该通知的处理之后,系统才会让新线程开始执行它的线程代码。(这也就是出现线程死锁的问题所在)
新建线程只会去调用已经被映射到系统进程空间的DLL中的DllMain函数。也就是说,当一个DLL映射到进程地址空间的 时候,已经存在的线程是不会调用该DLL的DlllMain函数的。
注意:
进程是不会让进程的主线程去调用DLL_THREAD_ATTACH值来调用DllMai函数的,在进程创建的时候,被映射到进程地址空间的任何DLL会收到DLL_PROCESS_ATTACH通知,而不是DLL_THREAD_ATTACH的通知。
DLL_THREAD_DETACH 3
当线程终止的首选方式就是让它的线程函数返回。这回使得系统调用ExitThread来终止线程。ExitThread告诉系统该线程想要终止,但系统不会立即终止,而会让这个线程用DLL_THREAD_DETACH来调用所有已经被映射DLL的DllMain函数。
注意:
DLL可能会妨碍线程的终止,只有当每个DLL都处理完DLL_THREAD_DETACH统治之后,系统才会真正的终止线程。
进程中的一个线程调用LoadLibrary来载入DLL这使得系统会用DLL_PROCESS_ATTACH来调用DLL的DllMain。当载入该DLL的线程退出的时候,会用DLL_THREAD_DETACH来调用DllMain函数。
在简单的了解DllMain的工作机制后,来分析一个为什么不能在DllMain中创建线程。
你先是想一下这样的情况:
这由于会将线程挂起等待的原因,会让在DllMain中创建线程会导致线程死锁的问题。
先看一下有问题的代码:
-
BOOL APIENTRY DllMain(HMODULE hModule,
-
DWORD ul_reason_for_call,
-
LPVOID lpReserved
-
)
-
{
-
HANDLE ThreadHandle =
NULL;
-
DWORD ThreadID =
0;
-
switch (ul_reason_for_call)
-
{
-
case DLL_PROCESS_ATTACH:
-
{
-
setlocale(LC_ALL,
"chinese");
-
BOOL v1;
-
MessageBox(
0, _T(
"Dll加载成功"),
0,
0);
-
-
ThreadHandle = CreateThread(
NULL,
0, ThreadProcedure_1,
NULL,
0, &ThreadID);
-
-
-
WaitForSingleObject(ThreadHandle, INFINITE);
-
CloseHandle(ThreadHandle);
-
break;
-
}
-
case DLL_THREAD_ATTACH:
-
MessageBox(
0, _T(
"DllDLL_THREAD_ATTACH"),
0,
0);
-
-
case DLL_THREAD_DETACH:
-
case DLL_PROCESS_DETACH:
-
break;
-
}
-
return TRUE;
-
}
我设置MessageBox是为了更简洁的可以看到DllMain 被调用。
当我加载DLL.dll时会出现以下结果
DLL_PROCESS_ATTACH被调用 MessageBox被弹出,由于MessageBox是阻塞的,所有这是还没有创建线程。如果正常情况下,当我创建线程的时候,也会弹出一个MessageBox来通知线程被创建。
但是结果是MessageBox没有弹出,说明这段代码是有问题的,已经方式了死锁。
原因是:
当DllMain收到DLL_PROCESS_ATTACH的时候,会创建一个线程。系统必须要用DLL_THREAD_ATTACH来再次调用DllMain函数。但是在老线程创建新线程的时候,会导致向新线程的DllMian发送DLL_PROCESS_ATTACH通知,由于老线程暂时没有对DLL的初始化,也就是对DllMian的调用没有完成,系统就会将新线程挂起,直到老线程完成调用,才会唤醒。但是老线程调用了WaitForSingleObject来等到新线程的执行,此时新线程已经被挂起在等到老线程执行完毕,但老线程也在等到新线程执行完毕,所有就发生了死锁的情况,两个线程都在互相等待对方的执行结果。
《Windows核心编程》书中提到了DisableThreadLibraryCalls函数,这函数是不让系统像某个指定DLL的DllMain函数发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知,其实这样也不并不能解决问题。
-
case DLL_PROCESS_ATTACH:
-
{
-
setlocale(LC_ALL,
"chinese");
-
BOOL v1;
-
MessageBox(
0, _T(
"Dll加载成功"),
0,
0);
-
DisableThreadLibraryCalls(hModule);
-
ThreadHandle = CreateThread(
NULL,
0, ThreadProcedure_1,
NULL,
0, &ThreadID);
-
-
-
WaitForSingleObject(ThreadHandle, INFINITE);
-
CloseHandle(ThreadHandle);
-
break;
-
}
-
case DLL_THREAD_ATTACH:
-
MessageBox(
0, _T(
"DllDLL_THREAD_ATTACH"),
0,
0);
-
break;
执行的结果和之前一样,可见并不能解决这个问题。
原因是:
当系统创建进程的时候,会同时创建一个锁。每个进程都有自己的锁,多个进程不会共享同一个锁。当进程中的线程调用映射到这个进程空间中的DLL的DllMain函数时,会通过这个锁来同步各个线程。
在程序调用CreateThread的时候,系统首先会创建线程内核对象和线程栈。然后系统内部调用WaitForSingleObject函数,并传入进程的互斥量对象句柄。当新线程得到互斥量所有权后,系统才会让新线程用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。只有这个时候,系统才会调用ReleaseMutex来放弃进程的互斥量所有权。由于系统时以这种方式运作的,所有添加DisableThreadLibraryCalls调用并不能防止线程锁。
所以解决方法就是不要调用WaitForSingleObject。