调试 CLR 挂起
Debugging a CLR hang
我已经上传了一个 WinDBG 会话的日志,我将参考它:https://pastebin.com/TvYD9500
所以,我正在调试客户报告的挂起问题。复制器是一个小的 C# 程序:
using System;
using System.Data.Odbc;
using System.Threading;
namespace ConnectionPoolingTest
{
class Program
{
static void Main(string[] args)
{
String connString = "DSN=DotNetUltraLightDSII";
using (OdbcConnection connnection = new OdbcConnection(connString))
{
connnection.Open();
connnection.Close();
}
}
}
}
我们销售用于构建 ODBC 驱动程序的框架,客户正在测试使用该框架构建的 ODBC 驱动程序。一个可能相关的细节是,他们正在使用一个允许用 C# 编写业务逻辑的组件,并且该组件是用 C++/CLI 编写的,以在我们的本机代码和客户代码之间架起桥梁(因此,ODBC 驱动程序DLL 是一个混合模式 DLL,它向 ODBC 驱动程序管理器公开一个 C 接口。
(如果需要,我也可以上传驱动程序二进制文件。)
在这个复制器(必须 运行 并且在所使用的 DSN 上启用了连接池)中发生的事情是,进程最终挂在一个带有堆栈的单线程上,如下所示:
RetAddr : Args to Child : Call Site
000007fe`fcea10dc : 00000000`00470000 00000000`770d0290 00000000`00000000 00000000`009ae8e0 : ntdll!ZwWaitForSingleObject+0xa
000007fe`f0298407 : 00000000`00999a98 00000000`770d5972 00000000`00000000 00000000`00000250 : KERNELBASE!WaitForSingleObjectEx+0x79
000007fe`f0294d04 : 00000000`00999a98 00000000`00a870e0 00000000`00999a68 00000000`00991a10 : comsvcs!UTSemReadWrite::LockWrite+0x90
000007fe`f0294ca8 : 00000000`00999a68 00000000`00999a98 00000000`00999a20 00000000`7717ba58 : comsvcs!CDispenserManager::~CDispenserManager+0x2c
000007fe`f02932a8 : 00000000`00999a20 00000000`00a871c0 00000000`77182e70 00000000`7717ba58 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::`scalar deleting destructor'+0x68
000007fe`f0293a00 : 000007fe`f0290000 00000000`00000001 00000000`00000001 00000000`00a87198 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::Release+0x20
000007fe`f02949aa : 00000000`00000000 00000000`00a87188 00000000`00992c20 00000000`00992c30 : comsvcs!ATL::CComModule::Term+0x35
000007fe`f0293543 : 00000000`00000000 00000000`00a87190 00000000`00000001 00000000`00a87278 : comsvcs!`dynamic atexit destructor for 'g_ModuleWrapper''+0x22
000007fe`f029355b : 00000000`00000001 00000000`00000000 000007fe`f0290000 00000000`76f515aa : comsvcs!CRT_INIT+0x96
00000000`7708778b : 000007fe`f0290000 00000000`00000000 00000000`00000001 00000000`7717ba58 : comsvcs!__DllMainCRTStartup+0x187
00000000`7708c1e0 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrShutdownProcess+0x1db
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x90
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
我找到了 UTSemReadWrite class 的一些源代码(但它似乎与我实际上 运行ning 有点不同):https://github.com/dotnet/coreclr/blob/616fea550548af750b575f3c304d1a9b4b6ef9a6/src/utilcode/utsem.cpp
将断点放入 UTSemReadWrite::LockWrite,我能够调试最后一个挂起的调用,发现原因是 m_dwFlag(用于原子性)是非零,所以它会等待一个事件(让拥有的线程在释放它时发出信号),它通过调用 UTSemReadWrite::GetWriteWaiterEvent 来实现,但是调用 创建 事件(此时没有其他线程......),然后等待它。轰,僵局。
通过程序集调试,我能够推断出 m_dwFlag 在对象中偏移了 4 个字节,并且在 UTSemReadWrite::UTSemReadWrite 中放置了一个断点,我能够得到地址挂起涉及的UTSemReadWrite实例,并在m_dwFlag.
上设置数据断点
这样做,我确实可以看到,具有线程函数 comsvcs!PingThread 的线程调用了 comsvcs!UTSemReadWrite::LockRead(并且可能获得了锁),然后在调用 comsvcs!UTSemReadWrite 之前被杀死: :解锁阅读。我以前见过这样的事情,它是由一个未处理的 SEH 异常杀死 PingThread 引起的,但是应用程序使用 setunhandledexceptionfilter() 防止崩溃,所以我认为可能是一些异常正在杀死线程,但事实证明是 CLR 本身!
RetAddr : Args to Child : Call Site
00000000`7708c198 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!ZwTerminateProcess+0xa
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x48
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
(这提出了一个问题;所以 ntdll!ZwTerminateProcess 不会实际上 终止进程?因为它显然是 returned 并调用 atexit 处理程序...我猜这是同名的不同函数?https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntddk/nf-ntddk-zwterminateprocess)
所以,我的问题是,我是否正确解释了调试器向我显示的内容?这实际上是 CLR 中的错误吗? CLR 不应该首先优雅地结束线程吗?
客户注意到,如果他在驱动程序中将其中一个线程创建为后台线程,则不会发生挂起,这很奇怪,因为当驱动程序运行时,即使是前台线程也应该很快停止已卸载(通过在驱动程序句柄上调用 SQLFreeHandle() 的终结器),除非终结器线程因某些原因而变慢,我猜?
发给我们的reproducer驱动后台线程基本上是
public Driver()
{
this.tokenSource= new CancellationTokenSource();
this.token = this.tokenSource.Token;
this.worker= new Thread(this.DoWork) { IsBackground = false };
this.worker.Start();
}
public override void Dispose()
{
this.tokenSource.Cancel();
this.worker.Join();
this.tokenSource.Dispose();
base.Dispose();
}
private void DoWork() {
while (!this.token.WaitHandle.WaitOne(200)) {
log(this.Log, "Doing some work....");
}
log(this.Log, "Done with work.");
}
and Dispose() 是被正确调用,and退出。
我不确定下一步该怎么做。
编辑:阅读 this 后,我感觉这是 CLR 的一个错误/'quirk'。在我的方案中,最后一个前台 .NET 线程位于 ODBC 驱动程序中。当 ODBC 驱动程序管理器调用 SQLFreeHandle
以卸载驱动程序时(从 windows 线程池中的某个线程或驱动程序管理器本身拥有的线程,不确定),这会导致驱动程序终止最后一个前台线。根据我从那篇文章中获得的对 CLR 关闭过程的理解,CLR 最终会在它有机会从它实际 return 之前终止调用 SQLFreeHandle
的线程,这是预期的行为。
但是那个线程似乎持有那个 UTSemReadWrite 锁,所以稍后在 atexit 处理期间它会死锁。
如果这实际上是 CLR 的错误,我关于如何解决这个问题的唯一想法是在对 SQLFreeHandle
的最终调用上启动另一个(前台).NET 线程,这将在一些之后结束自己超时(希望 SQLFreeHandle
线程能够释放它持有的任何锁),以延迟 CLR 关闭。如果最终阻止应用程序关闭,那不是很理想...
编辑:实际上,即使这个想法也行不通,因为这意味着 ODBC 驱动程序管理器可能会在线程正在执行驱动程序的代码时实际卸载驱动程序,从而导致崩溃。
我已经与 Microsoft 支持人员谈过,他们已经确认这是 comsvcs 组件的问题,他们可能会在 windows 的未来版本中修复该问题。如果他们告诉我他们已经修好了,我会更新这个。
我已经上传了一个 WinDBG 会话的日志,我将参考它:https://pastebin.com/TvYD9500
所以,我正在调试客户报告的挂起问题。复制器是一个小的 C# 程序:
using System;
using System.Data.Odbc;
using System.Threading;
namespace ConnectionPoolingTest
{
class Program
{
static void Main(string[] args)
{
String connString = "DSN=DotNetUltraLightDSII";
using (OdbcConnection connnection = new OdbcConnection(connString))
{
connnection.Open();
connnection.Close();
}
}
}
}
我们销售用于构建 ODBC 驱动程序的框架,客户正在测试使用该框架构建的 ODBC 驱动程序。一个可能相关的细节是,他们正在使用一个允许用 C# 编写业务逻辑的组件,并且该组件是用 C++/CLI 编写的,以在我们的本机代码和客户代码之间架起桥梁(因此,ODBC 驱动程序DLL 是一个混合模式 DLL,它向 ODBC 驱动程序管理器公开一个 C 接口。
(如果需要,我也可以上传驱动程序二进制文件。)
在这个复制器(必须 运行 并且在所使用的 DSN 上启用了连接池)中发生的事情是,进程最终挂在一个带有堆栈的单线程上,如下所示:
RetAddr : Args to Child : Call Site
000007fe`fcea10dc : 00000000`00470000 00000000`770d0290 00000000`00000000 00000000`009ae8e0 : ntdll!ZwWaitForSingleObject+0xa
000007fe`f0298407 : 00000000`00999a98 00000000`770d5972 00000000`00000000 00000000`00000250 : KERNELBASE!WaitForSingleObjectEx+0x79
000007fe`f0294d04 : 00000000`00999a98 00000000`00a870e0 00000000`00999a68 00000000`00991a10 : comsvcs!UTSemReadWrite::LockWrite+0x90
000007fe`f0294ca8 : 00000000`00999a68 00000000`00999a98 00000000`00999a20 00000000`7717ba58 : comsvcs!CDispenserManager::~CDispenserManager+0x2c
000007fe`f02932a8 : 00000000`00999a20 00000000`00a871c0 00000000`77182e70 00000000`7717ba58 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::`scalar deleting destructor'+0x68
000007fe`f0293a00 : 000007fe`f0290000 00000000`00000001 00000000`00000001 00000000`00a87198 : comsvcs!ATL::CComObjectCached<ATL::CComClassFactorySingleton<CDispenserManager> >::Release+0x20
000007fe`f02949aa : 00000000`00000000 00000000`00a87188 00000000`00992c20 00000000`00992c30 : comsvcs!ATL::CComModule::Term+0x35
000007fe`f0293543 : 00000000`00000000 00000000`00a87190 00000000`00000001 00000000`00a87278 : comsvcs!`dynamic atexit destructor for 'g_ModuleWrapper''+0x22
000007fe`f029355b : 00000000`00000001 00000000`00000000 000007fe`f0290000 00000000`76f515aa : comsvcs!CRT_INIT+0x96
00000000`7708778b : 000007fe`f0290000 00000000`00000000 00000000`00000001 00000000`7717ba58 : comsvcs!__DllMainCRTStartup+0x187
00000000`7708c1e0 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrShutdownProcess+0x1db
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x90
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
我找到了 UTSemReadWrite class 的一些源代码(但它似乎与我实际上 运行ning 有点不同):https://github.com/dotnet/coreclr/blob/616fea550548af750b575f3c304d1a9b4b6ef9a6/src/utilcode/utsem.cpp
将断点放入 UTSemReadWrite::LockWrite,我能够调试最后一个挂起的调用,发现原因是 m_dwFlag(用于原子性)是非零,所以它会等待一个事件(让拥有的线程在释放它时发出信号),它通过调用 UTSemReadWrite::GetWriteWaiterEvent 来实现,但是调用 创建 事件(此时没有其他线程......),然后等待它。轰,僵局。
通过程序集调试,我能够推断出 m_dwFlag 在对象中偏移了 4 个字节,并且在 UTSemReadWrite::UTSemReadWrite 中放置了一个断点,我能够得到地址挂起涉及的UTSemReadWrite实例,并在m_dwFlag.
上设置数据断点这样做,我确实可以看到,具有线程函数 comsvcs!PingThread 的线程调用了 comsvcs!UTSemReadWrite::LockRead(并且可能获得了锁),然后在调用 comsvcs!UTSemReadWrite 之前被杀死: :解锁阅读。我以前见过这样的事情,它是由一个未处理的 SEH 异常杀死 PingThread 引起的,但是应用程序使用 setunhandledexceptionfilter() 防止崩溃,所以我认为可能是一些异常正在杀死线程,但事实证明是 CLR 本身!
RetAddr : Args to Child : Call Site
00000000`7708c198 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!ZwTerminateProcess+0xa
000007fe`efb4ee58 : 00000000`00486b10 00000000`00000001 00000000`00482460 00000000`00000000 : ntdll!RtlExitUserProcess+0x48
000007fe`efb4efd4 : 00000000`00000000 000007fe`efb4efc0 ffffffff`00000000 00000000`004868a0 : mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x287
000007fe`eefa9535 : 00000000`0042f4b8 000007fe`ef53d6c0 00000000`0042f488 00000000`004868a0 : mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14
000007fe`eefa9495 : 00000000`00000000 00000000`0042f488 00000000`00000000 00000000`00000000 : clr!EEPolicy::ExitProcessViaShim+0x95
000007fe`eee83336 : 00000000`00000006 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!SafeExitProcess+0x9d
000007fe`eee61c51 : 00000000`01000000 00000000`0042f870 00000000`00000000 00000000`00000000 : clr!HandleExitProcessHelper+0x3e
000007fe`eee62034 : ffffffff`ffffffff 000007fe`eee62020 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0x101
000007fe`efb47b2d : 00000000`00000000 00000000`00000091 00000000`00000000 00000000`0042f7c8 : clr!CorExeMain+0x14
000007fe`efbe5b21 : 00000000`00000000 000007fe`eee62020 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
00000000`76f4556d : 000007fe`efb40000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x57
00000000`770a385d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
(这提出了一个问题;所以 ntdll!ZwTerminateProcess 不会实际上 终止进程?因为它显然是 returned 并调用 atexit 处理程序...我猜这是同名的不同函数?https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntddk/nf-ntddk-zwterminateprocess)
所以,我的问题是,我是否正确解释了调试器向我显示的内容?这实际上是 CLR 中的错误吗? CLR 不应该首先优雅地结束线程吗?
客户注意到,如果他在驱动程序中将其中一个线程创建为后台线程,则不会发生挂起,这很奇怪,因为当驱动程序运行时,即使是前台线程也应该很快停止已卸载(通过在驱动程序句柄上调用 SQLFreeHandle() 的终结器),除非终结器线程因某些原因而变慢,我猜?
发给我们的reproducer驱动后台线程基本上是
public Driver()
{
this.tokenSource= new CancellationTokenSource();
this.token = this.tokenSource.Token;
this.worker= new Thread(this.DoWork) { IsBackground = false };
this.worker.Start();
}
public override void Dispose()
{
this.tokenSource.Cancel();
this.worker.Join();
this.tokenSource.Dispose();
base.Dispose();
}
private void DoWork() {
while (!this.token.WaitHandle.WaitOne(200)) {
log(this.Log, "Doing some work....");
}
log(this.Log, "Done with work.");
}
and Dispose() 是被正确调用,and退出。
我不确定下一步该怎么做。
编辑:阅读 this 后,我感觉这是 CLR 的一个错误/'quirk'。在我的方案中,最后一个前台 .NET 线程位于 ODBC 驱动程序中。当 ODBC 驱动程序管理器调用 SQLFreeHandle
以卸载驱动程序时(从 windows 线程池中的某个线程或驱动程序管理器本身拥有的线程,不确定),这会导致驱动程序终止最后一个前台线。根据我从那篇文章中获得的对 CLR 关闭过程的理解,CLR 最终会在它有机会从它实际 return 之前终止调用 SQLFreeHandle
的线程,这是预期的行为。
但是那个线程似乎持有那个 UTSemReadWrite 锁,所以稍后在 atexit 处理期间它会死锁。
如果这实际上是 CLR 的错误,我关于如何解决这个问题的唯一想法是在对 SQLFreeHandle
的最终调用上启动另一个(前台).NET 线程,这将在一些之后结束自己超时(希望 SQLFreeHandle
线程能够释放它持有的任何锁),以延迟 CLR 关闭。如果最终阻止应用程序关闭,那不是很理想...
编辑:实际上,即使这个想法也行不通,因为这意味着 ODBC 驱动程序管理器可能会在线程正在执行驱动程序的代码时实际卸载驱动程序,从而导致崩溃。
我已经与 Microsoft 支持人员谈过,他们已经确认这是 comsvcs 组件的问题,他们可能会在 windows 的未来版本中修复该问题。如果他们告诉我他们已经修好了,我会更新这个。