记一次 .NET某工控自动化系统 崩溃分析
一:背景1. 讲故事
前些天微信上有位朋友找到我,说他的程序偶发崩溃,分析了个把星期也没找到问题,耗费了不少人力物力,让我能不能帮他看一下,给我申请了经费,哈哈,遇到这样的朋友就是爽快,刚好周二晚上给调试训练营的朋友分享 GC标记阶段 相关知识,而这个dump所展示的问题是对这块知识的一个很好的巩固,接下来我们开始分析吧。
二:WinDbg分析
1. 为什么会崩溃
要想找到崩溃原因,还是用老命令 !analyze -v ,输出如下:
0:005> !analyze -v
CONTEXT:(.ecxr)
eax=063ce258 ebx=07b90000 ecx=0063552e edx=0063552e esi=03070909 edi=03070909
eip=71954432 esp=063ce220 ebp=063ce23c iopl=0 nv up ei pl nz na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b efl=00010206
clr!WKS::gc_heap::mark_object_simple+0x12:
71954432 8b0f mov ecx,dword ptr ds:002b:03070909=????????
Resetting default scope
EXCEPTION_RECORD:(.exr -1)
ExceptionAddress: 71954432 (clr!WKS::gc_heap::mark_object_simple+0x00000012)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000001
NumberParameters: 2
Parameter: 00000000
Parameter: 03070909
Attempt to read from address 03070909
STACK_TEXT:
063ce23c 719543fc 063ce258 0a76cc88 71954260 clr!WKS::gc_heap::mark_object_simple+0x12
063ce25c 71950b62 0a76cc88 063cec88 00000000 clr!WKS::GCHeap::Promote+0xa8
...
063cec28 71950fa3 71950da0 063cec40 00000500 clr!Thread::StackWalkFrames+0x9d
063cec4c 7195103e 063cec88 00000002 00000000 clr!standalone::ScanStackRoots+0x43
063cec68 71954038 0079cb88 063cec88 00080101 clr!GCToEEInterface::GcScanRoots+0xdb
063cecc0 71953225 00080101 00000000 00000001 clr!WKS::gc_heap::mark_phase+0x17e
063cece0 7195355b 71f75da0 00000000 00000001 clr!WKS::gc_heap::gc1+0xae
063cecf8 71953665 71f75fb4 71f75fb4 00000000 clr!WKS::gc_heap::garbage_collect+0x367
063ced18 7195376a 00000000 00000000 71f75fb4 clr!WKS::GCHeap::GarbageCollectGeneration+0x1bd
...从卦中信息看,当前执行流处于GC标记阶段,并且是在各个线程栈上寻找用户根,在寻找的过程中踩到了坏内存,接下来需要捋一下是什么逻辑踩到的,可以用 u 反汇编一下。
0:005> u WKS::gc_heap::mark_object_simple
clr!WKS::gc_heap::mark_object_simple:
71954420 55 push ebp
71954421 8bec mov ebp,esp
71954423 83ec18 sub esp,18h
71954426 8b4508 mov eax,dword ptr
71954429 57 push edi
7195442a 8b38 mov edi,dword ptr
7195442c 89bde8ffffff mov dword ptr ,edi
71954432 8b0f mov ecx,dword ptr
...从汇编逻辑看,这是将方法的第一个参数进行解引用,参考 coreclr 的源码。
void gc_heap::mark_object_simple(uint8_t** po THREAD_NUMBER_DCL)
{
uint8_t* o = *po;
if (gc_mark1(o))
{
...
}
}结合C++代码,edi=03070909 就是上面的o,也就是需要标记的托管对象,但现在这个 o 是一个坏对象,那为什么会坏掉呢?
2. 为什么 o 坏掉了
按照过往经验肯定是托管堆损坏了,可以用 !verifyheap 观察下。
0:005> !verifyheap
No heap corruption detected.从卦中看,我去,托管堆居然是好的,过往经验在这个dump里被击的粉碎,接下来要往哪里突破呢? 可以观察下这个托管地址和当前的托管segment在空间距离上的特征,命令输出如下:
0:005> !address 03070909
Usage: <unknown>
Base Address: 02ca2000
End Address: 036f0000
Region Size: 00a4e000 (10.305 MB)
State: 00002000 MEM_RESERVE
Protect: <info not present at the target>
Type: 00020000 MEM_PRIVATE
Allocation Base: 026f0000
Allocation Protect: 00000004 PAGE_READWRITE
0:005> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x06ca7a7c
generation 1 starts at 0x06b91000
generation 2 starts at 0x026f1000
ephemeral segment allocation context: none
segment beginallocated size
026f0000026f100002c98f8c0x5a7f8c(5930892)
06b9000006b910000732b3d00x79a3d0(7971792)
Large object heap starts at 0x036f1000
segment beginallocated size
036f0000036f100003c78da00x587da0(5799328)
Total Size: Size: 0x12ca0fc (19702012) bytes.
------------------------------
GC Heap Size: Size: 0x12ca0fc (19702012) bytes.
0:005> !address
BaseAddr EndAddr+1 RgnSize Type State Protect Usage
-----------------------------------------------------------------------------------------------
...
+26f00002ca2000 5b2000 MEM_PRIVATE MEM_COMMITPAGE_READWRITE <unknown>[..........o.....]
2ca200036f0000 a4e000 MEM_PRIVATE MEM_RESERVE <unknown>
...说实话,有经验的朋友看到这卦中信息马上就知道是怎么回事了,步骤大概是这样的。
[*]03070909 曾经实打实的分配在 SOH 上
[*]GC 触发后,03070909 所在的 segment 被收缩,同时对象被移走。
[*]但不知为何,线程栈还保留了这个老地址 03070909,而不是新地址
出现这种情况的原因,大多是 C# 和 C++ 交互时没有把 03070909 给固定住(GCHandle.Alloc),导致GC触发对象移动之后,会存在两种情况的崩溃。
[*]C++ 层面的崩溃:因为此时的C++拿的地址不再有效了,导致在非托管层崩溃。
[*]CLR 层面的崩溃:线程如果在C++层面僵持,托管层GC触发时会误认为这个无效的地址还是一个有效的对象,进而在标记阶段导致程序崩溃。
有些朋友可能被我说懵了,画个简图如下:
由于这个dump属于第二种崩溃,即存在僵死的线程,接下来就是想办法找到这个线程。
3. 僵死的线程在哪里
如果你了解GC标记阶段的底层运作,我相信你很容易找出这个答案的,对,只需要找到 ScanStackRoots 函数的第一个参数即可,参考代码如下:
void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, ScanContext* sc)
{
Thread* pThread = NULL;
while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL)
{
ScanStackRoots(pThread, fn, sc);
}
}接下来上 windbg 在崩溃的线程栈上实操一下。
0:005> kb 8
# ChildEBP RetAddr Args to Child
00 063ce23c 719543fc 063ce258 0a76cc88 71954260 clr!WKS::gc_heap::mark_object_simple+0x12
01 063ce25c 71950b62 0a76cc88 063cec88 00000000 clr!WKS::GCHeap::Promote+0xa8
02 063ce274 71951a35 063cec40 0a76cc88 00000000 clr!GcEnumObject+0x37
03 063ce5d8 71950e6f 063ce920 063ce870 00000000 clr!EECodeManager::EnumGcRefs+0x72b
04 063ce628 717bfaa4 063ce650 063cec40 71950da0 clr!GcStackCrawlCallBack+0x139
05 063ce8f4 717bfbaa 063ce920 71950da0 063cec40 clr!Thread::StackWalkFramesEx+0x92
06 063cec28 71950fa3 71950da0 063cec40 00000500 clr!Thread::StackWalkFrames+0x9d
07 063cec4c 7195103e 063cec88 00000002 00000000 clr!standalone::ScanStackRoots+0x43
0:005> dp 063cec88 L1
063cec8808debbf8
0:005> !t
ThreadCount: 30
UnstartedThread:0
BackgroundThread: 29
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc ContextDomain Count Apt Exception
...
30 26 3e98 08debbf8 2b220 Preemptive00000000:00000000 0079cb88 0 MTA
...从卦中看,30号线程就是我苦苦寻找的僵死线程,接下来赶紧切过去看看,果然发现了C++的函数xxx.Driver.xxx,由于私密性,我就模糊一下了哈。
0:030> ~30s
eax=00000000 ebx=08debbf8 ecx=00000000 edx=00000000 esi=00000000 edi=00000244
eip=77872aac esp=0a76c9fc ebp=0a76ca6c iopl=0 nv up ei pl nz na pe nc
cs=0023ss=002bds=002bes=002bfs=0053gs=002b efl=00000206
ntdll!NtWaitForSingleObject+0xc:
77872aac c20c00 ret 0Ch
0:030> !clrstack
OS Thread Id: 0x3e98 (30)
Child SP IP Call Site
0a76cc18 77872aac
0a76cc0c 00aa8047 DomainBoundILStubClass.IL_STUB_PInvoke(UInt32, xxx ByRef)
0a76cc18 00aa6c67 xxx.Driver.xxx(UInt32, xxx ByRef)
0a76ccc0 00aa6c67 xxx.Driver.xxxFault(UInt32, System.String)
...既然发现了C++方法,最后还剩一个疑问,就是此时的03070909真的在非托管层吗?这个可以通过搜索它的线程栈地址。
0:030> s-d poi(@$teb+0x8) poi(@$teb+0x4) 03070909
0a76cc8803070909 728f5d01 68d8c642 5c654b42.....].rB..hBKe\从代码中可以看到确实是在xxx.Driver.xxxFault方法里传给了C++,有了这些信息接下来就是告诉朋友,重点关注下这个方法,捋一下逻辑。
三:总结
说实话这个dump分析起来还是有一定难度的,它考验着你对GC标记阶段玩法的底层理解,即使这位朋友是C#编程高手,分析了个把星期找不出问题是能够理解的,毕竟术业有专攻,很开心的是这位朋友因此加了.NET高级调试训练营,哈哈,以dump会友。
来源:https://www.cnblogs.com/huangxincheng/p/17989081
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页:
[1]