垃圾回收期间崩溃的原因

6

我最近一直在为一个使用了相当多C++/CLI模块的C#应用程序中的崩溃问题苦苦挣扎,这些模块大多是用来包装本地库以访问设备驱动程序的。崩溃并不总是容易重现,但我已经收集了半打的崩溃转储文件,显示程序总是在垃圾回收期间发生访问冲突而崩溃。这是本地调用堆栈和最后一个事件日志:

0:000> k
ChildEBP RetAddr  
0012d754 79f95a8f mscorwks!WKS::gc_heap::find_first_object+0x62
0012d7dc 79f933bb mscorwks!WKS::gc_heap::mark_through_cards_for_segments+0x493
0012d814 79f92cbf mscorwks!WKS::gc_heap::mark_phase+0xc3
0012d838 79f93245 mscorwks!WKS::gc_heap::gc1+0x62
0012d84c 79f92f5a mscorwks!WKS::gc_heap::garbage_collect+0x253
0012d878 79f94e26 mscorwks!WKS::GCHeap::GarbageCollectGeneration+0x1a9
0012d904 79f926ce mscorwks!WKS::gc_heap::try_allocate_more_space+0x15b
0012d918 79f92769 mscorwks!WKS::gc_heap::allocate_more_space+0x11
0012d938 79e73291 mscorwks!WKS::GCHeap::Alloc+0x3b

0:000> .lastevent
Last event: 7e8.88: Access violation - code c0000005 (first/second chance not available)
  debugger time: Mon Sep 26 11:34:53.646 2011 (UTC + 2:00)

所以,让我首先提出我的问题,并在下面给出更多细节。我的问题是:除了受管理堆损坏外,在垃圾回收期间还有其他导致崩溃的原因吗
现在稍微阐述一下,我之所以问这个问题,是因为我正在努力寻找破坏托管堆的代码,并且似乎找不到被覆盖的内存的模式。
我已经尝试注释掉所有“危险”的C++/CLI代码(特别是使用固定句柄的部分),但这并没有帮助。为了找到被覆盖的内存中的模式,我查看了崩溃点处的反汇编代码:
0:000> u .-a .+a
mscorwks!WKS::gc_heap::find_first_object+0x54:
79f935b9 89450c          mov     dword ptr [ebp+0Ch],eax
79f935bc 8bd0            mov     edx,eax
79f935be 8b02            mov     eax,dword ptr [edx]
79f935c0 83e0fe          and     eax,0FFFFFFFEh
79f935c3 f70000000080    test    dword ptr [eax],80000000h      <<<<CRASH
79f935c9 0f84b1000000    je      mscorwks!WKS::gc_heap::find_first_object+0x73

0:000> r
eax=00000000 ebx=01c81000 ecx=01c80454 edx=01c82fe0 esi=012f0000 edi=000027e1
eip=79f935c3 esp=0012d738 ebp=0012d754 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
mscorwks!WKS::gc_heap::find_first_object+0x62:
79f935c3 f70000000080    test    dword ptr [eax],80000000h ds:0023:00000000=????????

当试图解除引用为null的EAX寄存器时,崩溃发生了。现在从我的观察中可以看出EAX是从由EDX寄存器指向的内容加载的,因此我查看了存储在那里的地址:

0:000> dd @edx-10
01c82fd0  06542778 00000000 00000000 01c82494
01c82fe0  00000000 00000000 00000000 00000000
01c82ff0  01b641d0 00000000 00000000 01c82380

编辑:我现在意识到我的分析是错误的,缺乏对x86寻址模式的理解。

因此,我可以看到从地址01c82fed(存储在EDX中的值)开始,接下来的16个字节都是空的。 但是当查看另一个类似的崩溃转储时,我看到了以下内容:

0:000> dd @edx-10
018defd4  00000000 00000000 00000000 00000000
018defe4  00000000 00000000 018b468c 01742354
018deff4  00e0907f 00000000 00000000 00000000

因此,EDX指向的地址前面的16个字节和接下来的8个字节都是空的。我有其他崩溃转储文件也出现了同样的情况,但我没有看到任何模式,即似乎没有某些代码片段简单地覆盖了内存区域。

回到问题上,我想知道是否存在除了某个代码片段覆盖不该覆盖的内存之外的其他解释。或者在如何继续进行方面提供建议,我真的很迷茫...

(固定句柄会导致问题吗?我们有很多这样的句柄,我认为有趣的是我总是在崩溃点处看到137个固定句柄-不多不少-这对我来说是一个奇怪的巧合..)。

编辑:忘记提到我们正在使用.NET框架的3.5版本。我看到在.NET 4中当后台GC处于活动状态时会出现类似的崩溃报告(某处提到这是.NET的一个bug),但我认为这与此无关,因为据我所知,.NET 3.5中没有后台GC。


禁用该计算机上的反恶意软件。编写单元测试。 - Hans Passant
3个回答

2

不确定这是否有帮助,但通常不要使用析构函数或让GC处理非托管内存。相反,请使用Dispose模式,并将所有析构函数代码移动到finalizers中:

ref class MyClass
{
  UnsafeObject data;
  MyClass()
  {
    data = CreateUnsafeDataObject();
  }
  !MyClass()  // IDisposable.Dispose()
  {
    DeleteUnsafeDataObject(data);
  }
  ~MyClass()  // Destructor
  {

  }
}

这将在对象上实现IDisposable模式。调用Dispose以清除非托管数据,这样你最多就有更好的机会弄清楚到底发生了什么。


感谢您的回复。我刚刚重新检查了我们拥有的C++/CLI类,没有发现很多终结器,所以我不认为这是问题所在(但您提出了一个有效的观点);我更担心的是我们拥有的固定句柄,我已经通过代码检查来检查了它们,但我正在尝试使用!gchandles在WinDbg中直接查看它们,希望找到任何线索... - floyd73

2
很遗憾,我的问题有些误导性,因为我寻求的是除管理堆垃圾损坏之外的替代解释——最终发现这确实是问题所在(由于从未管理的结构体到已管理的结构体的不安全复制引起)。 问题现在已经解决,我会在这里发布我的发现,希望这样做没问题。

我可能有类似的问题。你是如何找到它/得出结论的? - Cohen
1
嗨,科恩,很抱歉回复晚了。经过长时间的测试和代码审查,我得出了这个结论。正如我所提到的,我一开始怀疑我们的代码正在破坏堆栈,因此我寻找其他解释垃圾收集器崩溃的方法。最终发现,我们其中一个结构体定义不正确(它指定的显式大小是错误的)。因此,如果你遇到同样的问题,我的建议是寻找任何可疑的“不安全”代码,并尝试通过暂时移除非关键组件来隔离问题。 - floyd73
谢谢,实际上我是通过使用WinDbg在本机代码中找到了它,并像您建议的那样通过注释本机依赖代码来解决了这个问题。 - Cohen

0

你可能在其中一个终结器中遇到了异常。我认为你需要逐个检查它们,因为终止队列中没有错误的容忍余地。如果你没有非托管代码,最好根本不要有终结器,而是手动调用Dispose。


谢谢您的回复,但我不认为异常是问题的原因(在WinDbg中的!threads输出中没有显示任何一个运行线程中的异常)。 - floyd73

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接