Нарушение прав доступа к финализатору RCW
Я использую COM-взаимодействие для создания управляемого плагина в неуправляемом приложении с использованием VS2012/.NET 4.5/Win8.1. Кажется, что все взаимодействие происходит нормально, но когда я закрываю приложение, я получаю исключение MDA, сообщающее мне, что AV произошли, пока выпускаются COM-объекты, которые RCW держали во время финализации.
Это стек вызовов:
clr.dll!MdaReportAvOnComRelease::ReportHandledException() + 0x91 bytes
clr.dll!**SafeRelease_OnException**() + 0x55 bytes
clr.dll!SafeReleasePreemp() + 0x312d5f bytes
clr.dll!RCW::ReleaseAllInterfaces() + 0xf3 bytes
clr.dll!RCW::ReleaseAllInterfacesCallBack() + 0x4f bytes
clr.dll!RCW::Cleanup() + 0x24 bytes
clr.dll!RCWCleanupList::ReleaseRCWListRaw() + 0x16 bytes
clr.dll!RCWCleanupList::ReleaseRCWListInCorrectCtx() + 0x9c bytes
clr.dll!RCWCleanupList::CleanupAllWrappers() + 0x2cd1b6 bytes
clr.dll!RCWCache::ReleaseWrappersWorker() + 0x277 bytes
clr.dll!AppDomain::ReleaseRCWs() + 0x120cb2 bytes
clr.dll!ReleaseRCWsInCaches() + 0x3f bytes
clr.dll!InnerCoEEShutDownCOM() + 0x46 bytes
clr.dll!WKS::GCHeap::**FinalizerThreadStart**() + 0x229 bytes
clr.dll!Thread::intermediateThreadProc() + 0x76 bytes
kernel32.dll!BaseThreadInitThunk() + 0xd bytes
ntdll.dll!RtlUserThreadStart() + 0x1d bytes
Я предполагаю, что Приложение уже уничтожило свои COM-объекты, некоторые ссылки на которые были переданы в управляемый плагин - и вызов IUnknown::Release, который делает RCW, заставляет его развиваться.
Я ясно вижу в окне вывода (VS), что приложение уже начало выгружать некоторые из своих DLL.
'TestHost.exe': Unloaded 'C:\Windows\System32\msls31.dll'
'TestHost.exe': Unloaded 'C:\Windows\System32\usp10.dll'
'TestHost.exe': Unloaded 'C:\Windows\System32\riched20.dll'
'TestHost.exe': Unloaded 'C:\Windows\System32\version.dll'
First-chance exception at 0x00000001400cea84 in VST3PluginTestHost.exe: 0xC0000005: Access violation reading location 0xffffffffffffffff.
First-chance exception at 0x00000001400cea84 in VST3PluginTestHost.exe: 0xC0000005: Access violation reading location 0xffffffffffffffff.
Managed Debugging Assistant 'ReportAvOnComRelease' has detected a problem in 'C:\Program Files\Steinberg\VST3PluginTestHost\VST3PluginTestHost.exe'.
Additional Information: An exception was caught but handled while releasing a COM interface pointer through Marshal.Release or Marshal.ReleaseComObject or implicitly after the corresponding RuntimeCallableWrapper was garbage collected. This is the result of a user refcount error or other problem with a COM object's Release. Make sure refcounts are managed properly. The COM interface pointer's original vtable pointer was 0x406975a8. While these types of exceptions are caught by the CLR, they can still lead to corruption and data loss so if possible the issue causing the exception should be addressed
Поэтому я решил, что сам буду управлять временем жизни и написал класс ComReference, который вызывает Marshal.ReleaseComObject. Это не сработало правильно, и после прочтения я должен согласиться с тем, что вызов Marshal.ReleaseComObject в сценарии, где ссылки передаются свободно, не является хорошей идеей. Marshal.ReleaseComObject считается опасным
Итак, вопрос: есть ли способ справиться с этой ситуацией, чтобы не вызывать AV при выходе из хост-приложения?
2 ответа
Есть только три реальных решения этой проблемы, и я думаю, что интерпретация статьи "Marshall.ReleaseComObject считается опасной" как "Не используйте Marshall.ReleaseComObject" может ввести вас в заблуждение. С таким же успехом вы могли бы сказать, что "не делитесь RCW свободно".
Ваши три решения:
1: Измените выполнение вашего хост-приложения, чтобы выгружать плагины перед тем, как оно будет выгружено. Это легче сказать, чем сделать. Если система плагинов хост-процесса включает в себя событие завершения работы, это было бы хорошим местом для решения этой проблемы. Все ваши службы, поддерживающие RCW, должны отключать их во время завершения работы.
2. Используйте Marshall.ReleaseComObject в шаблоне, подобном Dispose(), гарантируя, что объекты хранятся только в локальной области видимости, аналогично блоку using. Это просто реализовать, позволяет детерминистически освобождать ссылки COM и, как правило, является очень хорошим первым подходом.
3. Используйте посредника COM-объектов, который может раздавать экземпляры RCW с подсчетом ссылок, а затем освобождать эти объекты, когда их никто не использует. Убедитесь, что каждый потребитель этих объектов очищается перед выгрузкой приложения.
Вариант № 2 работает нормально, если вы не сохраняете / не делитесь ссылками на управляемый RCW. Я бы использовал #2 до тех пор, пока вы не определите, что ваш COM-объект имеет высокую стоимость активации и что кеширование / совместное использование имеет значение
Это проблема с собственным счетчиком ссылок COM. Ваш объект в настоящее время Release()
d из нативного кода с refcount=1, он уничтожается, затем приходит CLR и пытается Release()
Это. Вам нужно отследить, где счетчик ссылок работает неправильно. В CLR происходит сбой, потому что он выполняет очистку после завершения собственного кода.
Первый шаг - отследить тип объекта, который не учитывается должным образом. Я сделал это, запустив gflags.exe
против моего .exe
файл и включите "Трассировки стека пользовательского режима". "Куча полной страницы" также может помочь.
Запустите приложение в windbg
, Бежать .symfix
, Бежать bp clr!SafeReleasePreemp "r rcx; gc"; g
войти в интерфейс указателей. Когда происходит сбой, предыдущая запись журнала должна содержать указатель интерфейса, который уже был уничтожен. Бежать !heap -p -a [address of COM pointer]
и он напечатает стек, где он был выпущен.
Если вам не повезло, он не сразу выйдет из строя, и указатель на интерфейс, который вызывает проблемы, не будет самым последним журналом. Если вы можете запустить свой собственный COM в конфигурации отладки, это может помочь.
MS сделала заголовок RCW доступным. Участники m_pIdentity
(смещение 0x88 на x64) и m_aInterfaceEntries
(смещение 0x8 на x64) представляют интерес. RCW находится в @rdx
при въезде в SafeReleasePreemp
Следующим шагом является повторный запуск с точками останова на Interface::AddRef, Interface::QueryInterface и Interface::Release, чтобы увидеть, какая из них не соответствует. _ATL_DEBUG_INTERFACES
может помочь, если вы используете ATL.