DisableThreadLibraryCalls与DLLMain死锁
1、首先写个简单的DLL,用来验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { printf ( "DLL_PROCESS_ATTACH\n" ); } break ; case DLL_PROCESS_DETACH: { printf ( "DLL_PROCESS_DETACH\n" ); } break ; case DLL_THREAD_ATTACH: { printf ( "DLL_THREAD_ATTACH\n" ); } break ; case DLL_THREAD_DETACH: { printf ( "DLL_THREAD_DETACH\n" ); } break ; } return TRUE; } |
2、再写一个测试用的EXE:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
unsigned int __stdcall ThreadFun( PVOID pM); //int WinMain(HINSTANCE hInstance, // HINSTANCE hPrevInstance, // LPSTR lpCmdLine, // int nCmdShow) void main() { HMODULE hMod = LoadLibrary( "TestDLL.dll" ); HANDLE hThread = ( HANDLE )_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); if (!hThread) { return ; } WaitForSingleObject(hThread, INFINITE); FreeLibrary(hMod); getchar (); exit (0); // return 0; } unsigned int __stdcall ThreadFun( PVOID pM) { printf ( "线程ID号为%4d的子线程说:Hello World\n" , GetCurrentThreadId()); return 0; } |
3、你把这个EXE跑一遍就会发现,在线程被创建的时候,也会执行一次已经被加载的DLL的DLLMain,加载原因是DLL_THREAD_ATTACH,离开时也同理,最终显示结果如下:
4、如果你在原先的DLL代码中加入DisableThreadLibraryCalls调用,那么就不会在线程启动时去执行这个DLL的DLLMain了:
当然这里关于线程是否执行DLLMain的DLL_THREAD_ATTACH和DLL_THREAD_DETACH的,还有一些比较细节的逻辑,可以参考:
https://blog.csdn.net/breaksoftware/article/details/8142339
5、在DLL中会产生死锁的情况及原因
5.1、DLLMain中直接或者间接调用LoadLibrary:
起初我以为,假如我们在A.dll中的DllMain收到DLL_PROCESS_ATTACH时,加载了B.dll;而B.dll中的DllMain在收到DLL_PROCESS_ATTACH时又去加载A.dll,则产生了循环依赖,然而经试验检验,并不会,因为如果A.dll已经加载进EXE的内存了,B.dll就不会再去加载A.dll了,实验如下:
结果如下:
并没有发生死锁。第一这里没有发生调用循环;第二这里也没有发生死锁。
首先,为什么没有发生我们预计的循环调用呢?我们来一步步分析两个DLL相互加载的过程。
看调用堆栈发现,TestDLL2又调用了LdrLoadDll,看这架势是要递归加载的:
然而当我跟进去调试的时候,有某一个地方触发了返回,并没有调用LdrpRunInitializeRoutines,这个地方就是LdrpFindOrMapDll。但凡你要加载一个DLL,LdrpFindOrMapDll就会去查找是否已经加载过这个DLL了。
如果没有加载过某个DLL那么LdrpFindOrMapDll的最后一个参数会被写入1,而如果加载过了这个参数就会被写入0:
DLL的查找方式就是计算Hash并保存在一张全局的Hash表中:
其次,这里为什么没有发生死锁?原因很简单,我调试这里时:
发现TestDLL在再次调用LoadLibrary前后,LdrpLoaderLock的变化如下(命令是dt _RTL_CRITICAL_SECTION 770520c0):
也就是说这是同一个线程对LdrpLoaderLock上了锁,所以并没有死锁。那么再来验证下如果是另一个线程里,调用LoadLibrary是否会发生死锁呢?
我们把实验代码稍微改动下,让TestDLL在DLLMain中起一个线程://后发现,这里论证可能有误,因为即便是单独起一个线程,也会导致死锁,所以就证明不了这是LoadLibrary造成的了
exe中为了保证TestDLL不被Free掉,我就一直Sleep了:
如愿,运行起来就在某处卡死掉了。我们查看所有的临界区:
LockCount=1表示有一个线程正在等待这个临界区。然后我们查看这个临界区的详细信息:
0号线程正在等待1号线程拥有的临界区:
或者我们直接用!locks命令查看:
我们验证下0号线程:
再验证下1号线程:
这里总结下,DLLMain中并不是因为LoadLibrary中的循环调用造成的死锁,而是由于其它原因。什么原因?一个国外网站上给出了解释:
Your DllMain function runs inside the loader lock,one of the few times the OS lets you run code while one of its internal locks is held. This means that you must be extra careful not to violate a lock hierarchy in your DllMain; otherwise, you are asking for a deadlock.
The loader lock is taken by any function that needs to access the list of DLLs loaded into the process.loader lock This includes functions like GetModuleHandle and GetModuleFileName. If your DllMain enters a critical section or waits on a synchronization object, and that critical section or synchronization object is owned by some code that is in turn waiting for the loader lock, you just created a deadlock.
所以我们的结论是:
无法通过使用DisableThreadLibraryCalls解决死锁问题。因为,DisableThreadLibraryCalls只是用来防止EXE中起线程时重新执行DLLMain,但是你在主线程第一次Load TestDLL.dll时就已经出现死锁了。
DllMain中不当操作导致死锁问题的分析--DisableThreadLibraryCalls对DllMain中死锁的影响
《windows核心编程》作者在讨论DllMain执行序列化的时候,曾说过一个他的故事:他试图通过调用DisableThreadLibraryCalls以使得新线程不在调用DllMain从而解决死锁问题。但是该方案最后失败了。思考作者的思路,他可能一开始认为:因为线程要调用DllMain而加锁,于是windows在发现DllMain不用调用时就不用加锁了。本文将探讨DisableThreadLibraryCalls对DllMain死锁的影响。首先我们需要定位是什么函数调用了DllMain。为了方便分析,我设计了以下代码
- // 主程序
- while ( cin>>n ) {
- string strDllName;
- DWORD dwSleepTime = 0;
- switch(n) {
- case 0:{
- strDllName = "DllWithDisableThreadLibraryCalls_A";
- dwSleepTime = 100000;
- }break;
- ……
- case 4:{
- strDllName = "DllWithoutDisableThreadLibraryCalls_A";
- dwSleepTime = 3000;
- }break;
- ……
- default:
- break;
- }
- HMODULE h = LoadLibraryA(strDllName.c_str());
- Sleep(dwSleepTime);
- if ( NULL != h ) {
- FreeLibrary(h);
- }
- }
- case DLL_PROCESS_ATTACH:{
- printf("DLL DllWithDisableThreadLibraryCalls_A:\tProcess attach (tid = %d)\n", tid);
- DisableThreadLibraryCalls(hModule);
- HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);
- CloseHandle(hThread);
- }break;
- static DWORD WINAPI ThreadCreateInDllMain(LPVOID) {
- return 0;
- }
我们先让我们进程加载DllWithoutDisableThreadLibraryCalls_A.dll(输入4)以找到DllMain加载的关键路径。为了达到这个目的,我将设置几个断点:
Exe中
DLL中
在LoadLibrary之后Sleep了一下,是为了让工作线程有机会执行起来。
在执行到Sleep之后,程序中断在DLL中。我们看调用堆栈
我将关注下从ntdll进入DllWithoutDisableThreadLibraryCalls_A.dll的逻辑调用。双击_LdrpCallInitRoutine这行查看其汇编
- _LdrpCallInitRoutine@16:
- 7C921176 55 push ebp
- 7C921177 8B EC mov ebp,esp
- 7C921179 56 push esi
- 7C92117A 57 push edi
- 7C92117B 53 push ebx
- 7C92117C 8B F4 mov esi,esp
- 7C92117E FF 75 14 push dword ptr [ebp+14h]
- 7C921181 FF 75 10 push dword ptr [ebp+10h]
- 7C921184 FF 75 0C push dword ptr [ebp+0Ch]
- 7C921187 FF 55 08 call dword ptr [ebp+8]
- 7C92118A 8B E6 mov esp,esi
- 7C92118C 5B pop ebx
- 7C92118D 5F pop edi
- 7C92118E 5E pop esi
- 7C92118F 5D pop ebp
- 7C921190 C2 10 00 ret 10h
- _LdrpInitializeThread@4:
- 7C9399A2 6A 40 push 40h
- 7C9399A4 68 D8 99 93 7C push 7C9399D8h
- 7C9399A9 E8 1D 4F FF FF call __SEH_prolog (7C92E8CBh)
- 7C9399AE 64 A1 18 00 00 00 mov eax,dword ptr fs:[00000018h]
- 7C9399B4 8B 58 30 mov ebx,dword ptr [eax+30h]
- 7C9399B7 89 5D DC mov dword ptr [ebp-24h],ebx
- 7C9399BA 80 3D C4 E0 99 7C 00 cmp byte ptr [_LdrpShutdownInProgress (7C99E0C4h)],0
- 7C9399C1 0F 85 C3 00 00 00 jne _LdrpInitializeThread@4+136h (7C939A8Ah)
- 7C9399C7 E8 59 FF FF FF call _LdrpAllocateTls@0 (7C939925h)
- 7C9399CC 8B 43 0C mov eax,dword ptr [ebx+0Ch]
- 7C9399CF 8B 70 14 mov esi,dword ptr [eax+14h]
- 7C9399D2 EB 1C jmp _LdrpInitializeThread@4+30h (7C9399F0h)
- 7C9399D4 90 nop
- 7C9399D5 90 nop
- 7C9399D6 90 nop
- 7C9399D7 90 nop
- 7C9399D8 ?? db ffh
- 7C9399D9 ?? db ffh
- 7C9399DA ?? db ffh
- 7C9399DB FF 00 inc dword ptr [eax]
- 7C9399DD 00 00 add byte ptr [eax],al
- 7C9399DF 00 B5 F3 95 7C FF add byte ptr [ebp-836A0Dh],dh
- 7C9399E5 ?? db ffh
- 7C9399E6 ?? db ffh
- 7C9399E7 FF 00 inc dword ptr [eax]
- 7C9399E9 00 00 add byte ptr [eax],al
- 7C9399EB 00 62 5C add byte ptr [edx+5Ch],ah
- 7C9399EE 95 xchg eax,ebp
- 7C9399EF 7C 89 jl __LdrpInitialize@12+259h (7C93997Ah)
- 7C9399F1 75 E4 jne 7C9399D7
- 7C9399F3 8B 4B 0C mov ecx,dword ptr [ebx+0Ch]
- 7C9399F6 6A 14 push 14h
- 7C9399F8 58 pop eax
- 7C9399F9 03 C8 add ecx,eax
- 7C9399FB 3B F1 cmp esi,ecx
- 7C9399FD 74 7E je _LdrpInitializeThread@4+0E0h (7C939A7Dh)
- 7C9399FF 8B 4B 08 mov ecx,dword ptr [ebx+8]
- 7C939A02 3B 4E 10 cmp ecx,dword ptr [esi+10h]
- 7C939A05 74 6F je _LdrpInitializeThread@4+0C9h (7C939A76h)
- 7C939A07 8B 56 2C mov edx,dword ptr [esi+2Ch]
- 7C939A0A F7 C2 00 00 04 00 test edx,40000h
- 7C939A10 75 64 jne _LdrpInitializeThread@4+0C9h (7C939A76h)
- 7C939A12 8B 4E 14 mov ecx,dword ptr [esi+14h]
- 7C939A15 89 4D E0 mov dword ptr [ebp-20h],ecx
- 7C939A18 85 C9 test ecx,ecx
- 7C939A1A 74 5A je _LdrpInitializeThread@4+0C9h (7C939A76h)
- 7C939A1C F7 C2 00 00 08 00 test edx,80000h
- 7C939A22 74 52 je _LdrpInitializeThread@4+0C9h (7C939A76h)
- 7C939A24 F6 C2 04 test dl,4
- 7C939A27 74 4D je _LdrpInitializeThread@4+0C9h (7C939A76h)
- 7C939A29 89 45 B0 mov dword ptr [ebp-50h],eax
- 7C939A2C C7 45 B4 01 00 00 00 mov dword ptr [ebp-4Ch],1
- 7C939A33 33 C0 xor eax,eax
- 7C939A35 8D 7D B8 lea edi,[ebp-48h]
- 7C939A38 AB stos dword ptr es:[edi]
- 7C939A39 AB stos dword ptr es:[edi]
- 7C939A3A AB stos dword ptr es:[edi]
- 7C939A3B FF 76 40 push dword ptr [esi+40h]
- 7C939A3E 8D 45 B0 lea eax,[ebp-50h]
- 7C939A41 50 push eax
- 7C939A42 E8 51 77 FE FF call _RtlActivateActivationContextUnsafeFast@8 (7C921198h)
- 7C939A47 33 FF xor edi,edi
- 7C939A49 89 7D FC mov dword ptr [ebp-4],edi
- 7C939A4C 66 39 7E 32 cmp word ptr [esi+32h],di
- 7C939A50 0F 85 93 0C 01 00 jne _LdrpInitializeThread@4+96h (7C94A6E9h)
- 7C939A56 80 3D C4 E0 99 7C 00 cmp byte ptr [_LdrpShutdownInProgress (7C99E0C4h)],0
- 7C939A5D 75 0E jne _LdrpInitializeThread@4+0C0h (7C939A6Dh)
- 7C939A5F 57 push edi
- 7C939A60 6A 02 push 2
- 7C939A62 FF 76 10 push dword ptr [esi+10h]
- 7C939A65 FF 75 E0 push dword ptr [ebp-20h]
- 7C939A68 E8 09 77 FE FF call _LdrpCallInitRoutine@16 (7C921176h)
- 7C939A6D 83 4D FC FF or dword ptr [ebp-4],0FFFFFFFFh
- 7C939A71 E8 1D FF FF FF call _LdrpInitializeThread@4+0D6h (7C939993h)
- 7C939A76 8B 36 mov esi,dword ptr [esi]
- 7C939A78 E9 73 FF FF FF jmp _LdrpInitializeThread@4+30h (7C9399F0h)
- 7C939A7D 80 3D DC E0 99 7C 00 cmp byte ptr [_LdrpImageHasTls (7C99E0DCh)],0
- 7C939A84 0F 85 7D C1 01 00 jne _LdrpInitializeThread@4+0E9h (7C955C07h)
- 7C939A8A E8 77 4E FF FF call __SEH_epilog (7C92E906h)
- 7C939A8F C2 04 00 ret 4
由以上流程可以发现,标红色的7C939A10前一句和7C9399FD前一句是重要的跳转条件判断。它们分别是
- 7C939A0A F7 C2 00 00 04 00 test edx,40000h
- 7C9399F6 6A 14 push 14h
- 7C9399F8 58 pop eax
- 7C9399F9 03 C8 add ecx,eax
- 7C9399FB 3B F1 cmp esi,ecx
Kernel32中的DisableThreadLibraryCalls底层调用了ntdll中的LdrDisableThreadCalloutsForDll函数。我们看下LdrDisableThreadCalloutsForDll代码
- NTSTATUS
- LdrDisableThreadCalloutsForDll (
- IN PVOID DllHandle
- )
- /*++
- Routine Description:
- This function disables thread attach and detach notification
- for the specified DLL.
- Arguments:
- DllHandle - Supplies a handle to the DLL to disable.
- Return Value:
- TBD
- --*/
- {
- NTSTATUS st;
- PLDR_DATA_TABLE_ENTRY LdrDataTableEntry;
- st = STATUS_SUCCESS;
- try {
- if ( LdrpInLdrInit == FALSE ) {
- RtlEnterCriticalSection((PRTL_CRITICAL_SECTION)NtCurrentPeb()->LoaderLock);
- }
- if ( LdrpShutdownInProgress ) {
- return STATUS_SUCCESS;
- }
- if (LdrpCheckForLoadedDllHandle(DllHandle, &LdrDataTableEntry)) {
- if ( LdrDataTableEntry->TlsIndex ) {
- st = STATUS_DLL_NOT_FOUND;
- }
- else {
- LdrDataTableEntry->Flags |= LDRP_DONT_CALL_FOR_THREADS;
- }
- }
- }
- finally {
- if ( LdrpInLdrInit == FALSE ) {
- RtlLeaveCriticalSection((PRTL_CRITICAL_SECTION)NtCurrentPeb()->LoaderLock);
- }
- }
- return st;
- }
我们再看下LdrpInitializeThread的代码
- VOID LdrpInitializeThread( IN PCONTEXT Context )
- {
- PPEB Peb;
- PLDR_DATA_TABLE_ENTRY LdrDataTableEntry;
- PDLL_INIT_ROUTINE InitRoutine;
- PLIST_ENTRY Next;
- Peb = NtCurrentPeb();
- if ( LdrpShutdownInProgress ) {
- return;
- }
- LdrpAllocateTls();
- Next = Peb->Ldr->InMemoryOrderModuleList.Flink;
- while (Next != &Peb->Ldr->InMemoryOrderModuleList) {
- LdrDataTableEntry
- = (PLDR_DATA_TABLE_ENTRY)
- (CONTAINING_RECORD(Next, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
- //
- // 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 ) {
- if ( !LdrpShutdownInProgress ) {
- LdrpCallTlsInitializers(LdrDataTableEntry->DllBase,DLL_THREAD_ATTACH);
- }
- }
- #if defined (WX86)
- if (!Wx86ProcessInit ||
- LdrpRunWx86DllEntryPoint(InitRoutine,
- NULL,
- LdrDataTableEntry->DllBase,
- DLL_THREAD_ATTACH,
- NULL
- ) == STATUS_IMAGE_MACHINE_TYPE_MISMATCH)
- #endif
- {
- if ( !LdrpShutdownInProgress ) {
- LdrpCallInitRoutine(InitRoutine,
- LdrDataTableEntry->DllBase,
- DLL_THREAD_ATTACH,
- NULL);
- }
- }
- }
- }
- }
- }
- Next = Next->Flink;
- }
- //
- // If the image has tls than call its initializers
- //
- if ( LdrpImageHasTls && !LdrpShutdownInProgress ) {
- LdrpCallTlsInitializers(NtCurrentPeb()->ImageBaseAddress,DLL_THREAD_ATTACH);
- }
- }
- VOID
- LdrpInitialize (
- IN PCONTEXT Context,
- IN PVOID SystemArgument1,
- IN PVOID SystemArgument2
- )
- {
- ……
- Peb->LoaderLock = (PVOID)&LoaderLock;
- if ( !RtlTryEnterCriticalSection(&LoaderLock) ) {
- if ( LoaderLockInitialized ) {
- RtlEnterCriticalSection(&LoaderLock);
- }
- else {
- //
- // drop into a 30ms delay loop
- //
- DelayValue.QuadPart = Int32x32To64( 30, -10000 );
- while ( !LoaderLockInitialized ) {
- NtDelayExecution(FALSE,&DelayValue);
- }
- RtlEnterCriticalSection(&LoaderLock);
- }
- }
- ……
- LdrpInitializeThread(Context);
- ……
- RtlLeaveCriticalSection(&LoaderLock);
- …
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.