一,问题介绍
最近在做代码重构,今早对前两天重构的某个组件进行自测时,突然发现停止程序运行时,出现了卡死,进程退不出,查看日志发现是FreeLibrary没有走完。打开任务管理器-》性能-》资源监视器,找到进程,右键分析分析等待链,结果如下:
出现了死锁,16800线程是主线程(调用FreeLibrary),12128是DLL中的工作线程,两个线程互相等待,导致死锁,进程退不出。
二,问题分析
打开Windbg,配好pdb文件和源码路径,attach到该进程上,输入!locks
命令,查看当前进程锁的情况:
可以发现锁771f20c0处于死锁状态,它此时被30f8线程占有。再输入~*kb
查看所有线程,看看哪个线程正在等待锁771f20c0:
线程42d4在等待锁771f20c0,而锁771f20c0被线程30f8占有,我们再看看30f8线程:
它也在等待,不过从代码中看得出来它在等待线程42d4的退出,而42d4线程又在等待771f20c0锁的释放,可是锁771f20c0被30f8线程占有,由此产生的死锁,导致进程退不出来:
可是我们在线程30f8中并没有看到771f20c0锁的相关信息,这个锁是在42d2线程调用LdrShutdownThread时进行等待的,我们可以在win2K中找到它的实现:
VOID
LdrShutdownThread (
VOID
)
/*++
Routine Description:
This function is called by a thread that is terminating cleanly.
It's purpose is to call all of the processes DLLs to notify them
that the thread is detaching.
Arguments:
None
Return Value:
None.
--*/
{
PPEB Peb;
PLDR_DATA_TABLE_ENTRY LdrDataTableEntry;
PDLL_INIT_ROUTINE InitRoutine;
PLIST_ENTRY Next;
Peb = NtCurrentPeb();
RtlEnterCriticalSection(&LoaderLock);
try {
//
// Go in reverse order initialization order and build
// the unload list
//
Next = Peb->Ldr->InInitializationOrderModuleList.Blink;
while ( Next != &Peb->Ldr->InInitializationOrderModuleList) {
LdrDataTableEntry
= (PLDR_DATA_TABLE_ENTRY)
(CONTAINING_RECORD(Next,LDR_DATA_TABLE_ENTRY,InInitializationOrderLinks));
Next = Next->Blink;
//
// Walk through the entire list looking for
// entries. For each entry, that has an init
// routine, call it.
//
if (Peb->ImageBaseAddress != LdrDataTableEntry->DllBase) {
if ( !(LdrDataTableEntry->Flags & LDRP_DONT_CALL_FOR_THREADS)) {
InitRoutine = (PDLL_INIT_ROUTINE)LdrDataTableEntry->EntryPoint;
if (InitRoutine && (LdrDataTableEntry->Flags & LDRP_PROCESS_ATTACH_CALLED) ) {
if (LdrDataTableEntry->Flags & LDRP_IMAGE_DLL) {
if ( LdrDataTableEntry->TlsIndex ) {
LdrpCallTlsInitializers(LdrDataTableEntry->DllBase,DLL_THREAD_DETACH);
}
#if defined (WX86)
if (!Wx86ProcessInit ||
LdrpRunWx86DllEntryPoint(InitRoutine,
NULL,
LdrDataTableEntry->DllBase,
DLL_THREAD_DETACH,
NULL
) == STATUS_IMAGE_MACHINE_TYPE_MISMATCH)
#endif
{
LdrpCallInitRoutine(InitRoutine,
LdrDataTableEntry->DllBase,
DLL_THREAD_DETACH,
NULL);
}
}
}
}
}
}
//
// If the image has tls than call its initializers
//
if ( LdrpImageHasTls ) {
LdrpCallTlsInitializers(NtCurrentPeb()->ImageBaseAddress,DLL_THREAD_DETACH);
}
LdrpFreeTls();
} finally {
RtlLeaveCriticalSection(&LoaderLock);
}
}
锁771f20c0也就是LoaderLock,LoaderLock是dll加载和释放时微软内部给的一把锁(就是一个临界区),它在LoadLibrary和FreeLibrary时都会用到,我们以FreeLibrary来看:
在红色函数内部就会去获取LoaderLock这把锁,继续走会到DLL的各个析构函数里面。而在我们的析构函数里会通知线程退出,并用WaitForSingleObject等待各个创建的线程退出,最终也会调用_LdrShutdownThread,而这个函数内部也在等待LoaderLock这把锁,由此导致了死锁。
三,问题总结
对于DLL中的线程释放,最好提供一个导出函数,函数内部专门处理各个线程的释放,由外部调用方主动调用该函数,切记不可使用全局类或者静态类的析构函数来进行线程的释放退出。
参考文献:
1,https://blog.csdn.net/breaksoftware/article/details/8163663
2,《windows核心编程》第二十章