翼度科技»论坛 编程开发 .net 查看内容

记一次 .NET某工控自动化系统 崩溃分析

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
一:背景

1. 讲故事

前些天微信上有位朋友找到我,说他的程序偶发崩溃,分析了个把星期也没找到问题,耗费了不少人力物力,让我能不能帮他看一下,给我申请了经费,哈哈,遇到这样的朋友就是爽快,刚好周二晚上给调试训练营的朋友分享 GC标记阶段 相关知识,而这个dump所展示的问题是对这块知识的一个很好的巩固,接下来我们开始分析吧。
二:WinDbg分析

1. 为什么会崩溃

要想找到崩溃原因,还是用老命令 !analyze -v ,输出如下:
  1. 0:005> !analyze -v
  2. CONTEXT:  (.ecxr)
  3. eax=063ce258 ebx=07b90000 ecx=0063552e edx=0063552e esi=03070909 edi=03070909
  4. eip=71954432 esp=063ce220 ebp=063ce23c iopl=0         nv up ei pl nz na pe nc
  5. cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
  6. clr!WKS::gc_heap::mark_object_simple+0x12:
  7. 71954432 8b0f            mov     ecx,dword ptr [edi]  ds:002b:03070909=????????
  8. Resetting default scope
  9. EXCEPTION_RECORD:  (.exr -1)
  10. ExceptionAddress: 71954432 (clr!WKS::gc_heap::mark_object_simple+0x00000012)
  11.    ExceptionCode: c0000005 (Access violation)
  12.   ExceptionFlags: 00000001
  13. NumberParameters: 2
  14.    Parameter[0]: 00000000
  15.    Parameter[1]: 03070909
  16. Attempt to read from address 03070909
  17. STACK_TEXT:  
  18. 063ce23c 719543fc     063ce258 0a76cc88 71954260 clr!WKS::gc_heap::mark_object_simple+0x12
  19. 063ce25c 71950b62     0a76cc88 063cec88 00000000 clr!WKS::GCHeap::Promote+0xa8
  20. ...
  21. 063cec28 71950fa3     71950da0 063cec40 00000500 clr!Thread::StackWalkFrames+0x9d
  22. 063cec4c 7195103e     063cec88 00000002 00000000 clr!standalone::ScanStackRoots+0x43
  23. 063cec68 71954038     0079cb88 063cec88 00080101 clr!GCToEEInterface::GcScanRoots+0xdb
  24. 063cecc0 71953225     00080101 00000000 00000001 clr!WKS::gc_heap::mark_phase+0x17e
  25. 063cece0 7195355b     71f75da0 00000000 00000001 clr!WKS::gc_heap::gc1+0xae
  26. 063cecf8 71953665     71f75fb4 71f75fb4 00000000 clr!WKS::gc_heap::garbage_collect+0x367
  27. 063ced18 7195376a     00000000 00000000 71f75fb4 clr!WKS::GCHeap::GarbageCollectGeneration+0x1bd
  28. ...
复制代码
从卦中信息看,当前执行流处于GC标记阶段,并且是在各个线程栈上寻找用户根,在寻找的过程中踩到了坏内存,接下来需要捋一下是什么逻辑踩到的,可以用 u 反汇编一下。
  1. 0:005> u WKS::gc_heap::mark_object_simple
  2. clr!WKS::gc_heap::mark_object_simple:
  3. 71954420 55              push    ebp
  4. 71954421 8bec            mov     ebp,esp
  5. 71954423 83ec18          sub     esp,18h
  6. 71954426 8b4508          mov     eax,dword ptr [ebp+8]
  7. 71954429 57              push    edi
  8. 7195442a 8b38            mov     edi,dword ptr [eax]
  9. 7195442c 89bde8ffffff    mov     dword ptr [ebp-18h],edi
  10. 71954432 8b0f            mov     ecx,dword ptr [edi]
  11. ...
复制代码
从汇编逻辑看,这是将方法的第一个参数进行解引用,参考 coreclr 的源码。
  1. void gc_heap::mark_object_simple(uint8_t** po THREAD_NUMBER_DCL)
  2. {
  3.         uint8_t* o = *po;
  4.         if (gc_mark1(o))
  5.         {
  6.         ...
  7.         }
  8. }
复制代码
结合C++代码,edi=03070909 就是上面的o,也就是需要标记的托管对象,但现在这个 o 是一个坏对象,那为什么会坏掉呢?
2. 为什么 o 坏掉了

按照过往经验肯定是托管堆损坏了,可以用 !verifyheap 观察下。
  1. 0:005> !verifyheap
  2. No heap corruption detected.
复制代码
从卦中看,我去,托管堆居然是好的,过往经验在这个dump里被击的粉碎,接下来要往哪里突破呢? 可以观察下这个托管地址和当前的托管segment在空间距离上的特征,命令输出如下:
  1. 0:005> !address 03070909
  2. Usage:                  <unknown>
  3. Base Address:           02ca2000
  4. End Address:            036f0000
  5. Region Size:            00a4e000 (  10.305 MB)
  6. State:                  00002000          MEM_RESERVE
  7. Protect:                <info not present at the target>
  8. Type:                   00020000          MEM_PRIVATE
  9. Allocation Base:        026f0000
  10. Allocation Protect:     00000004          PAGE_READWRITE
  11. 0:005> !eeheap -gc
  12. Number of GC Heaps: 1
  13. generation 0 starts at 0x06ca7a7c
  14. generation 1 starts at 0x06b91000
  15. generation 2 starts at 0x026f1000
  16. ephemeral segment allocation context: none
  17. segment     begin  allocated      size
  18. 026f0000  026f1000  02c98f8c  0x5a7f8c(5930892)
  19. 06b90000  06b91000  0732b3d0  0x79a3d0(7971792)
  20. Large object heap starts at 0x036f1000
  21. segment     begin  allocated      size
  22. 036f0000  036f1000  03c78da0  0x587da0(5799328)
  23. Total Size:              Size: 0x12ca0fc (19702012) bytes.
  24. ------------------------------
  25. GC Heap Size:    Size: 0x12ca0fc (19702012) bytes.
  26. 0:005> !address
  27.   BaseAddr EndAddr+1 RgnSize     Type       State                 Protect             Usage
  28. -----------------------------------------------------------------------------------------------
  29. ...
  30. +  26f0000  2ca2000   5b2000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [..........o.....]
  31.    2ca2000  36f0000   a4e000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
  32. ...
复制代码
说实话,有经验的朋友看到这卦中信息马上就知道是怎么回事了,步骤大概是这样的。

  • 03070909 曾经实打实的分配在 SOH 上
  • GC 触发后,03070909 所在的 segment 被收缩,同时对象被移走。
  • 但不知为何,线程栈还保留了这个老地址 03070909,而不是新地址
出现这种情况的原因,大多是 C# 和 C++ 交互时没有把 03070909 给固定住(GCHandle.Alloc),导致GC触发对象移动之后,会存在两种情况的崩溃。

  • C++ 层面的崩溃:因为此时的C++拿的地址不再有效了,导致在非托管层崩溃。
  • CLR 层面的崩溃:线程如果在C++层面僵持,托管层GC触发时会误认为这个无效的地址还是一个有效的对象,进而在标记阶段导致程序崩溃。
有些朋友可能被我说懵了,画个简图如下:

由于这个dump属于第二种崩溃,即存在僵死的线程,接下来就是想办法找到这个线程。
3. 僵死的线程在哪里

如果你了解GC标记阶段的底层运作,我相信你很容易找出这个答案的,对,只需要找到 ScanStackRoots 函数的第一个参数即可,参考代码如下:
  1. void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, ScanContext* sc)
  2. {
  3.         Thread* pThread = NULL;
  4.         while ((pThread = ThreadStore::GetThreadList(pThread)) != NULL)
  5.         {
  6.                 ScanStackRoots(pThread, fn, sc);
  7.         }
  8. }
复制代码
接下来上 windbg 在崩溃的线程栈上实操一下。
  1. 0:005> kb 8
  2. # ChildEBP RetAddr      Args to Child              
  3. 00 063ce23c 719543fc     063ce258 0a76cc88 71954260 clr!WKS::gc_heap::mark_object_simple+0x12
  4. 01 063ce25c 71950b62     0a76cc88 063cec88 00000000 clr!WKS::GCHeap::Promote+0xa8
  5. 02 063ce274 71951a35     063cec40 0a76cc88 00000000 clr!GcEnumObject+0x37
  6. 03 063ce5d8 71950e6f     063ce920 063ce870 00000000 clr!EECodeManager::EnumGcRefs+0x72b
  7. 04 063ce628 717bfaa4     063ce650 063cec40 71950da0 clr!GcStackCrawlCallBack+0x139
  8. 05 063ce8f4 717bfbaa     063ce920 71950da0 063cec40 clr!Thread::StackWalkFramesEx+0x92
  9. 06 063cec28 71950fa3     71950da0 063cec40 00000500 clr!Thread::StackWalkFrames+0x9d
  10. 07 063cec4c 7195103e     063cec88 00000002 00000000 clr!standalone::ScanStackRoots+0x43
  11. 0:005> dp 063cec88 L1
  12. 063cec88  08debbf8
  13. 0:005> !t
  14. ThreadCount:      30
  15. UnstartedThread:  0
  16. BackgroundThread: 29
  17. PendingThread:    0
  18. DeadThread:       0
  19. Hosted Runtime:   no
  20.                                                                          Lock  
  21.        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
  22.        ...
  23.        30   26 3e98 08debbf8     2b220 Preemptive  00000000:00000000 0079cb88 0     MTA
  24.        ...
复制代码
从卦中看,30号线程就是我苦苦寻找的僵死线程,接下来赶紧切过去看看,果然发现了C++的函数xxx.Driver.xxx,由于私密性,我就模糊一下了哈。
  1. 0:030> ~30s
  2. eax=00000000 ebx=08debbf8 ecx=00000000 edx=00000000 esi=00000000 edi=00000244
  3. eip=77872aac esp=0a76c9fc ebp=0a76ca6c iopl=0         nv up ei pl nz na pe nc
  4. cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
  5. ntdll!NtWaitForSingleObject+0xc:
  6. 77872aac c20c00          ret     0Ch
  7. 0:030> !clrstack
  8. OS Thread Id: 0x3e98 (30)
  9. Child SP       IP Call Site
  10. 0a76cc18 77872aac [InlinedCallFrame: 0a76cc18]
  11. 0a76cc0c 00aa8047 DomainBoundILStubClass.IL_STUB_PInvoke(UInt32, xxx ByRef)
  12. 0a76cc18 00aa6c67 [InlinedCallFrame: 0a76cc18] xxx.Driver.xxx(UInt32, xxx ByRef)
  13. 0a76ccc0 00aa6c67 xxx.Driver.xxxFault(UInt32, System.String)
  14. ...
复制代码
既然发现了C++方法,最后还剩一个疑问,就是此时的03070909真的在非托管层吗?这个可以通过搜索它的线程栈地址。
  1. 0:030> s-d poi(@$teb+0x8) poi(@$teb+0x4) 03070909
  2. 0a76cc88  03070909 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】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具