首页 > 其他分享 >记一次 .NET某工控自动化系统 崩溃分析

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

时间:2024-01-28 21:05:09浏览次数:22  
标签:00000000 工控 gc heap 自动化 NET 03070909 WKS clr

一:背景

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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
clr!WKS::gc_heap::mark_object_simple+0x12:
71954432 8b0f            mov     ecx,dword ptr [edi]  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[0]: 00000000
   Parameter[1]: 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 [ebp+8]
71954429 57              push    edi
7195442a 8b38            mov     edi,dword ptr [eax]
7195442c 89bde8ffffff    mov     dword ptr [ebp-18h],edi
71954432 8b0f            mov     ecx,dword ptr [edi]
...

从汇编逻辑看,这是将方法的第一个参数进行解引用,参考 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     begin  allocated      size
026f0000  026f1000  02c98f8c  0x5a7f8c(5930892)
06b90000  06b91000  0732b3d0  0x79a3d0(7971792)
Large object heap starts at 0x036f1000
 segment     begin  allocated      size
036f0000  036f1000  03c78da0  0x587da0(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
-----------------------------------------------------------------------------------------------
...
+  26f0000  2ca2000   5b2000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [..........o.....]
   2ca2000  36f0000   a4e000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
...

说实话,有经验的朋友看到这卦中信息马上就知道是怎么回事了,步骤大概是这样的。

  • 03070909 曾经实打实的分配在 SOH 上
  • GC 触发后,03070909 所在的 segment 被收缩,同时对象被移走。
  • 但不知为何,线程栈还保留了这个老地址 03070909,而不是新地址

出现这种情况的原因,大多是 C# 和 C++ 交互时没有把 03070909 给固定住(GCHandle.Alloc),导致GC触发对象移动之后,会存在两种情况的崩溃。

  1. C++ 层面的崩溃:因为此时的C++拿的地址不再有效了,导致在非托管层崩溃。
  2. CLR 层面的崩溃:线程如果在C++层面僵持,托管层GC触发时会误认为这个无效的地址还是一个有效的对象,进而在标记阶段导致程序崩溃。

有些朋友可能被我说懵了,画个简图如下:

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

由于这个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
063cec88  08debbf8

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 Context  Domain   Count Apt Exception
       ...
       30   26 3e98 08debbf8     2b220 Preemptive  00000000: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=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=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 [InlinedCallFrame: 0a76cc18] 
0a76cc0c 00aa8047 DomainBoundILStubClass.IL_STUB_PInvoke(UInt32, xxx ByRef)
0a76cc18 00aa6c67 [InlinedCallFrame: 0a76cc18] 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
0a76cc88  03070909 728f5d01 68d8c642 5c654b42  .....].rB..hBKe\

从代码中可以看到确实是在xxx.Driver.xxxFault方法里传给了C++,有了这些信息接下来就是告诉朋友,重点关注下这个方法,捋一下逻辑。

三:总结

说实话这个dump分析起来还是有一定难度的,它考验着你对GC标记阶段玩法的底层理解,即使这位朋友是C#编程高手,分析了个把星期找不出问题是能够理解的,毕竟术业有专攻,很开心的是这位朋友因此加了.NET高级调试训练营,哈哈,以dump会友。

记一次 .NET某工控自动化系统 崩溃分析_非托管_02



标签:00000000,工控,gc,heap,自动化,NET,03070909,WKS,clr
From: https://blog.51cto.com/u_15353947/9454188

相关文章

  • 使用 NuGet.Server 创建和部署 ASP.NET Web 应用程序搭建私有Nuget服务器
    使用NuGet.Server创建和部署ASP.NETWeb应用程序搭建私有Nuget服务器在VisualStudio中,选择“新建>文件>”Project,搜索“ASP.NETWeb应用程序(.NETFramework)”,选择C#的匹配模板。将“框架”设置为“.NETFramework4.6”。为应用程序提供除NuGet.Server......
  • .NET问题:The remote certificate is invalid because of errors in the certificate c
    错误信息:内部异常1:HttpRequestException:TheSSLconnectioncouldnotbeestablished,seeinnerexception.内部异常2:AuthenticationException:Theremotecertificateisinvalidbecauseoferrorsinthecertificatechain:NotTimeValid解决方法跳过验证......
  • Part 2:NetOps与传统网络运营的区别
    转载在对NetOps和传统网络运营进行比较时,网络团队专注于自动化可重复的任务以纠正问题,而不是手动引入新任务。过去几年,IT语言发生了变化。以前,NetOps主要用作网络运营的简单缩写。现在,它的含义已发展为DevOps时代重新构想的网络运营,并且成为网络管理员、经理和工程师们越来越......
  • Part 3:为什么 NetOps 是通往 AIOps 的桥梁
    转载网络运营(或NetOps)团队,传统上使用性能监控工具来管理企业网络的运行状况和性能。然而,网络使用量的增长加上分散的网络部署,导致许多人寻求替代的性能监控方法,包括使用人工智能进行IT运营(AIOps)。本文比较了NetOps实践中的传统性能监控方法,并讨论了团队最终转向采用AIOps的原......
  • UI自动化搭建背景及优劣势分析
    经常有人会问,什么样的项目才适合进行UI自动化测试呢?UI自动化测试相当于模拟手工测试,通过程序去操作页面上的控件。而在实际测试过程中,经常会遇到无法找到控件,或者因控件定义变更而带来的维护成本等问题。哪些场景下适合UI自动化测试介入呢?产品比较稳定,不会有频繁的需求变更;需要频繁......
  • .net core8 knife4 web api文档
    knife4官网:https://github.com/luoyunchong/IGeekFan.AspNetCore.Knife4jUI1.创建.netcore8webapi项目2.nuget包管理添加如下包:Swashbuckle.AspNetCore.SwaggerSwashbuckle.AspNetCore.SwaggerGenIGeekFan.AspNetCore.Knife4jUI  3.在项目右键-属性-生成-......
  • UI自动化搭建背景及优劣势分析
    经常有人会问,什么样的项目才适合进行UI自动化测试呢?UI自动化测试相当于模拟手工测试,通过程序去操作页面上的控件。而在实际测试过程中,经常会遇到无法找到控件,或者因控件定义变更而带来的维护成本等问题。哪些场景下适合UI自动化测试介入呢?产品比较稳定,不会有频繁的需求变更;需要......
  • Part 4:NetOps 策略如何影响企业网络团队?
    转载网络运营(NetOps)是一种采用DevOps中应用程序开发人员流行的敏捷协作软件开发框架并将其与网络管理联系起来的方法。现代网络正在从静态配置、以硬件为中心的架构转向灵活、以软件为中心的系统。这一举措凸显了DevOps和网络管理之间配对的必要性。网络团队正在有机地转向NetO......
  • Part 5:NetOps团队的 SecOps和网络安全基础知识
    转载为了弥合NetOps和SecOps团队之间的差距,网络专业人员应该了解安全基础知识,包括不同类型的破坏者和可用的安全服务。尽管网络和安全对于彼此以及业务运营都至关重要,但网络运营和安全运营团队(分别为NetOps和SecOps)通常是孤立的。如果不完全了解SecOps团队的工作,NetOps团队可能无......
  • .NetCore开发人员首选框架---Bridge(Abp-VNext + Vue3)
    bridge系统是基于Abp-VNext+Vue3开发的一套前后端分离的通用权限管理系统,不论是单体服务,还是微服务都可在此基础上自由扩展,此框架组合可以说是集成了.netcore在BS架构领域最前沿的技术,框架简介如下:##......