记一次 .NET某工控自动化系统 崩溃分析
内容提要
本文介绍了作者如何使用WinDbg分析程序崩溃问题,发现是由于坏内存导致的。作者还发现了僵死的线程,并确定崩溃发生在C++方法中。最后,作者总结了dump分析的难度,并鼓励关注崩溃发生的方法。
延伸解读
GC标记阶段崩溃的典型特征
本次崩溃发生在CLR的GC标记阶段,具体是在mark_object_simple方法中访问了无效地址03070909。该地址原本是托管对象,但GC触发后对象被移动,而线程栈仍保留旧地址,导致标记时解引用失败。这提醒我们,当崩溃栈涉及GC内部函数时,应优先考虑托管堆状态和对象移动问题。
托管堆未损坏时的排查方向
使用!verifyheap命令确认托管堆未损坏,排除了堆破坏的常见原因。此时应关注地址空间属性:03070909位于MEM_RESERVE区域,说明它已不是有效托管对象。结合!eeheap -gc查看GC堆段信息,可判断该地址曾属于SOH段,后因段收缩被移走。这提示我们,堆未损坏不代表对象地址有效,需结合内存区域状态分析。
C++/CLR交互中的对象固定问题
崩溃根源是C#与C++交互时未使用GCHandle固定对象。当GC移动对象后,C++层仍持有旧地址,可能导致两种崩溃:非托管层直接访问无效地址,或CLR在GC标记阶段误将旧地址当作有效对象。本例属于后者,因僵死线程在C++方法中等待,GC扫描其栈时触发了崩溃。这强调了跨层传递对象引用时必须固定。
定位僵死线程与问题方法
通过ScanStackRoots函数的参数找到僵死线程(30号线程),其调用栈显示停留在xxx.Driver.xxxFault等C++方法中。进一步搜索线程栈,确认03070909地址确实由该方法传递给C++。这为解决问题提供了明确方向:检查该方法的对象传递逻辑,确保在跨层调用期间对象被正确固定。
Q&A
WinDbg分析中,!analyze -v 命令显示了什么关键信息?
!analyze -v 显示异常记录为访问违规(Access violation),异常地址在 clr!WKS::gc_heap::mark_object_simple+0x12,尝试读取地址 03070909,表明崩溃发生在GC标记阶段。
为什么托管堆没有损坏,但GC标记阶段却访问了坏内存?
因为对象 03070909 曾经在SOH上,GC触发后对象被移动,但线程栈仍保留旧地址。由于C#和C++交互时未用GCHandle固定对象,导致C++层持有无效地址,GC标记时误将旧地址当作有效对象,从而访问坏内存。
如何找到导致崩溃的僵死线程?
通过分析崩溃线程栈,找到 ScanStackRoots 函数的第一个参数,即线程对象地址。使用 dp 命令查看该地址,再用 !t 命令列出所有线程,匹配 ThreadOBJ 找到对应线程ID(如30号线程),然后切换过去查看其调用栈。
僵死线程的调用栈显示了什么?
僵死线程的调用栈显示它正在执行C++方法,如 xxx.Driver.xxxFault,并且通过P/Invoke调用非托管代码。在栈上搜索地址 03070909,确认该地址是在 xxx.Driver.xxxFault 方法中传递给C++的。
这个dump分析案例的主要教训是什么?
主要教训是:在C#和C++交互时,必须使用GCHandle固定托管对象,防止GC移动对象导致非托管代码持有无效地址。同时,dump分析需要深入理解GC标记阶段的底层机制,即使经验丰富的开发者也可能需要较长时间才能定位问题。
GC标记阶段中,mark_object_simple 函数的作用是什么?
mark_object_simple 函数用于标记托管对象。它接收一个指向对象指针的指针,解引用获取对象地址,然后进行标记。如果对象地址无效(如已移动),则会导致访问违规崩溃。