在Dll中创建线程?No,大错特错

56 篇文章 2 订阅

前几天的时候,遇到一个面试 ,问我在Dll中的忌讳有什么?我回答的是不能在dll  main函数中创建线程和加载动态库,然后下一个问题也接着来了,为什么不能?我说会造成死锁,但是具体的是什么原因造成的死锁,当时只记得看过《Windows 核心编程》,书中有过介绍,但是忘记了,无非就是因为同步问题,线程间会造成互相阻塞状态。具体是什么,今天回来分析一下。

 

Windows 核心编程中的原话是这样说的:

 

   DLL 必须使用DllMain函数来对自己进行初始化。DllMain函数执行的时候,同一个地址空间的中的其他DLL可能还没有初始化,也就是没有调用其他DLL 的DllMain函数,所以我们应该尽量避免去使用从其他DLL中导入的函数。此外,还应该避免在DllMain中调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能会产生循环依赖。

为了更好的理解,这里先介绍一下DllMain 函数:

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中创建线程。

你先是想一下这样的情况:

  • 一个进程有两个线程A和B,进程地址空间还映射了一个DLL.dll的DLL。两个线程都调用CreateThread来创建新的线程C和D。
  • 当线程A调用CreateThread来创建线程C的时候,系统会用DLL_THREAD_ATTACH来调用DLL.dll中的DllMian函数,当新建线程C执行DllMain中的代码的时候,线程B调用CreateThread来创建线程D的时候,系统也必须调用DLL.dll中的DllMain函数,但是这次是让要让线程D来执行DllMain。
  • 这个时候系统就会对DllMain函数的调用序列化,它会将线程D挂起,知道线程C执行完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。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值