Bei der Entwicklung von neuen Funktionen in einem Modul bekam ich irgendwann einen ASSERT. Diesen ASSERT hatte ich in einem Cache eingebaut. Der ASSERT sprang an, wenn bei Programmende der Cache nicht sauber aufgeräumt wurde. Also eigentlich in der Entwicklungsphase nichts ungewöhnliches. Irgendwo wurde also eine Funktion zum Aufräumen nicht aufgerufen.

Aber was mich in diesem Moment extrem stutzig machte, war, dass in der Debugausgabe meines VisualStudios keine Memory Leaks angezeigt wurden :!: Das machte irritierte mich nun schon sehr. Erster Verdacht. Evtl. hat ja jemand die Leakprüfung für den Cache ausgeschaltet (siehe AfxEnableMemoryTracking). Aber eine kurze Prüfung ergab, dass dem nicht so ist.

Also den Test noch mal ausgeführt. Diesmal wieder der ASSERT und zusätzlich der Dump. “Oha” dachte ich “Hier ist aber was richtig faul!”

Nachdem ich immer wieder andere Testszenarien verwendet habe, erschienen mal die Leaks und mal erschienen sie nicht. Und nach einiger Zeit kaum ich dahinter, dass immer wenn PlaySound in meiner Debug Version der Anwendung verwendet wurde, kein Leak-Check erfolgte. Wurde PlaySound nicht verwendet war alles gut und die Leaks wurden ausgegeben.

Jetzt ging es ans eingemachte. Schnell isolierte ich folgende DLLs die zusätzlich beim ersten PlaySound geladen wurden.

'xyz.exe': Loaded 'C:\Windows\SysWOW64\MMDevAPI.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\propsys.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\wdmaud.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\ksuser.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\avrt.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\setupapi.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\cfgmgr32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\devobj.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\AudioSes.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.drv', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\msacm32.dll', Symbols loaded (source information stripped).
'xyz.exe': Loaded 'C:\Windows\SysWOW64\midimap.dll', Symbols loaded (source information stripped).

Und nach etwas weiter debuggen kam ich dahinter, dass der DllMain Code von setupapi.dll beim DETACH_PROCESS den Prozess sofort terminiert und nicht alle DLLs einen DETACH_PROCESS Aufruf erhalten. Aber der Code für die Leak-Detection der MFC liegt in der MFCx.DLL und wird durch eine statische Variable ausgelöst, die beim Entladen der MFCx.DLL dann zur Ausgabe der Speicherlecks führt. (Siehe PROCESS_LOCAL(_AFX_DEBUG_STATE, afxDebugState) und AfxDiagnosticInit).

Eine genauere Analyse des Stacktraces ergab folgendes Bild:

ntdll.dll!_ZwTerminateProcess@8()  + 0x5 bytes	
ntdll.dll!_RtlpWaitOnCriticalSection@8()  + 0x1d38f bytes	
ntdll.dll!_RtlEnterCriticalSection@4()  + 0x16a38 bytes	
setupapi.dll!_pSetupInitGlobalFlags@4()  + 0x1fc bytes	
setupapi.dll!_pSetupGetGlobalFlags@0()  + 0xe2 bytes	
setupapi.dll!_FlushWVTCache@0()  + 0x2f bytes	
setupapi.dll!_DestroyDeviceInfoSet@8()  + 0x3f bytes	
setupapi.dll!_SetupDiDestroyDeviceInfoList@4()  + 0x44 bytes	
MMDevAPI.dll!CDeviceEnumerator::FinalRelease()  + 0x78 bytes	
MMDevAPI.dll!ATL::CComObjectCached::~CComObjectCached()  + 0x3c bytes	
MMDevAPI.dll!ATL::CComObjectCached::`scalar deleting destructor'()  + 0xd bytes	
MMDevAPI.dll!ATL::CComObjectCached::Release()  + 0xf0c6 bytes	
MMDevAPI.dll!ATL::CComClassFactorySingleton::~CComClassFactorySingleton()  + 0x18 bytes	
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::`scalar deleting destructor'()  + 0x1a bytes	
MMDevAPI.dll!ATL::CComObjectNoLock<ATL::CComClassFactorySingleton >::Release()  + 0xe0e3 bytes	
MMDevAPI.dll!ATL::CAtlComModule::Term()  + 0x27 bytes	
MMDevAPI.dll!__CRT_INIT@12()  + 0x26e6 bytes	
MMDevAPI.dll!__CRT_INIT@12()  + 0x2588 bytes	
ntdll.dll!_LdrpCallInitRoutine@16()  + 0x14 bytes	
ntdll.dll!_LdrShutdownProcess@0()  + 0x141 bytes	
ntdll.dll!_RtlExitUserProcess@4()  + 0x74 bytes	
kernel32.dll!75eb7a0d() 	
msvcr100d.dll!__crtExitProcess(int status=0)  Line 709	C
msvcr100d.dll!doexit(int code=0, int quick=0, int retcaller=0)  Line 621 + 0x9 bytes	C
msvcr100d.dll!exit(int code=0)  Line 393 + 0xd bytes	C
xyz.exe!__tmainCRTStartup()  Line 568	C

Ursache war, dass MMDevAPI.DLL in seinem DllMain Code ausführt in der setupapi.dll, die aber bereits den DETACH_PROCESS abgearbeitet hat. Das grundsätzliche Problem wird hier in diesem Blog Artikel geschildert: http://blogs.msdn.com/b/oldnewthing/archive/2005/05/23/421024.aspx
Mit eigenen Worten: Die MMDevAPI.dll ruft Funktionen in einer DLL auf, die bereits alle Ihren Speicher und Ressourcen freigegeben hat. Und wie es hier im Code so aussieht, versucht die SetupAPI.DLL wieder Ressourcen zu akquirieren, die eben bereits schon freigegeben wurden, weil eine DLL sie erneut benutzt.

Die Folge ein “Crash” in DllMain, die der Windows-Lader aber abfängt und sofort mit einer Terminierung des Prozesses ahndet. D.h. nun aber, dass nicht alle DLLs, die noch einen DllMain Aufruf mit DETACH_PROCESS erhalten müssten,  auch an die Reihe kommen.

Etwas weitere Recherche ergab, dass dieses Problem auch in den MSDN Foren bereits diskutiert wurde inkl. einer möglichen Lösung.
http://social.msdn.microsoft.com/Forums/vstudio/en-US/8cb1847d-3218-4610-9cb8-6905bd255ff5/no-dllprocessdetach-after-calling-playsound-on-windows-7-64bit

Die Lösung ist erstaunlich einfach: Wenn vor der Benutzung von PlaySound explizit die SetupAPI.DLL geladen wird, dann verläuft der Rest der Deinitialisierung vollkommen normal. SetupAPI.DLL wird nicht entladen, weil die DLL durch den LoadLibrary Aufruf die DLL im Speicher sperrt. MMDevAPI.DLL kann erfolgreich selbst aufräumen und der Code läuft nicht mehr ins Nirvana.

Hier handelt sich offensichtlich um einen Bug in Windows 7 (Windows 8 konnte ich dies bzgl. nicht testen).
Dieser Bug kann natürlich auch empfindlichere Probleme mit sich bringen, wenn Ressourcen betroffen sind, die nicht automatisch mit Prozessende freigegeben werden und wenn diese Ressourcen ausschließlich in der DllMain bei einem DETACH_PROCESS behandelt werden.