记一次 .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=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触发对象移动之后,会存在两种情况的崩溃。
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
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某工控自动化系统 崩溃分析的更多相关文章
- 记一次 .NET 某工控软件 内存泄露分析
一:背景 1.讲故事 上个月 .NET调试训练营 里的一位老朋友给我发了一个 8G 的dump文件,说他的程序内存泄露了,一时也没找出来是哪里的问题,让我帮忙看下到底是怎么回事,毕竟有了一些调试功底也 ...
- 记一次 .NET 某工控数据采集平台 线程数 爆高分析
一:背景 1. 讲故事 前几天有位朋友在 B站 加到我,说他的程序出现了 线程数 爆高的问题,让我帮忙看一下怎么回事,截图如下: 说来也奇怪,这些天碰到了好几起关于线程数无缘无故的爆高,不过那几个问题 ...
- 记一次 .NET 某工控自动化控制系统 卡死分析
一:背景 1. 讲故事 前段时间遇到了好几起关于窗体程序的 进程加载锁 引发的 程序卡死 和 线程暴涨 问题,这种 dump 分析难度较大,主要涉及到 Windows操作系统 和 C++ 的基础知识, ...
- 记一次 .NET 某工控视觉软件 非托管泄漏分析
一:背景 1.讲故事 最近分享了好几篇关于 非托管内存泄漏 的文章,有时候就是这么神奇,来求助的都是这类型的dump,一饮一啄,莫非前定.让我被迫加深对 NT堆, 页堆 的理解,这一篇就给大家再带来一 ...
- 记一次 .NET 某工控MES程序 崩溃分析
一:背景 1.讲故事 前几天有位朋友找到我,说他的程序出现了偶发性崩溃,已经抓到了dump文件,Windows事件日志显示的崩溃点在 clr.dll 中,让我帮忙看下是怎么回事,那到底怎么回事呢? 上 ...
- 记一次 .NET 某医疗器械 程序崩溃分析
一:背景 1.讲故事 前段时间有位朋友在微信上找到我,说他的程序偶发性崩溃,让我帮忙看下怎么回事,上面给的压力比较大,对于这种偶发性崩溃,比较好的办法就是利用 AEDebug 在程序崩溃的时候自动抽一 ...
- 记一次 .NET 某医疗住院系统 崩溃分析
一:背景 1. 讲故事 最近收到了两起程序崩溃的dump,查了下都是经典的 double free 造成的,蛮有意思,这里就抽一篇出来分享一下经验供后面的学习者避坑吧. 二:WinDbg 分析 1. ...
- Wireshark工控协议
Wireshark是一个强大开源流量与协议分析工具,除了传统网络协议解码外,还支持众多主流和标准工控协议的分析与解码. 序号 协议类型 源码下载 简介 1 Siemens S7 https://git ...
- 开源纯C#工控网关+组态软件(十)移植到.NET Core
一. 引子 写这个开源系列已经十来篇了.自从十年前注册博客园以来,关注了张善友.老赵.xiaotie.深蓝色右手等一众大牛,也围观了逗比的吉日嘎啦.精密顽石等形形色色的园友.然而整整十年一篇文章都 ...
- 【转】工控老鬼】西门子S7200入门&精通【1】S7200硬件大全
转载地址:http://blog.sina.com.cn/s/blog_669692a601016i5f.html 工控老鬼提醒以下的信息和资料可能不全或者不准确,如有疑问可以查阅西门子中国网 ...
随机推荐
- 云图说丨带你了解GaussDB(for Redis)双活解决方案
摘要:GaussDB(for Redis)推出了双活解决方案,基于GaussDB NoSQL统一架构,通过两个数据库实例之间的数据同步,达成数据的一致性. 本文分享自华为云社区<[云图说]一张图 ...
- 什么是MircoPython?
摘要:互联网玩家为了让Python这样的容易学,简单易学.社区API丰富的语言可以在嵌入式领域用上,逐渐开始了一轮Python上嵌入式的迁移,这样就有了今天的主角--MircoPython. 本文分享 ...
- 电商运营该如何做 AB 测试
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 近年,电商行业进入了一个新的发展阶段,一方面电商市场规模持续扩大,另一方面直播电商.即时零售.社区团购等新兴电商业 ...
- SpringBoot Docker Skywalking agent 不生效
SpringBoot Skywalking agent 通过 Dockfile 配置 不生效 ENTRYPOINT ["java","-Djava.security.eg ...
- peewee update和save性能分析
背景 python项目中使用了peewee这款orm框架,在对数据库更新时有两种语法,分别是save和update方法.有同事说从peewee的日志来看,update比save更快,于是做了一个简单的 ...
- 白话文解析LiteFlow的理念是什么?什么时候用该怎么用?干货满满
官网:https://liteflow.cc/ Gitee:https://gitee.com/dromara/liteFlow Github:https://github.com/dromara/l ...
- 【Boost】CMake中引用Boost库
概述 在macOS开发时常常使用Boost库,若项目使用CMake进行组织管理和编译,需要掌握在CMake中实现Boost库的引用的基本语法.本片博客结合自己在实际使用过程中的经验进行总结,以期回顾和 ...
- Codeforces Round #706 Editorial
1496A. Split it! 类回文判断,只要 k = 0 或者 \(s[1,k] 和 s[n - k + 1,n]\)是回文即可 特判情况 n < 2 * k + 1 为 NO int m ...
- 去重N皇后
题目:将上下对称.左右对称棋局.主副对角线对称棋局和旋转后重复视为重复,则要求输出去重后的N皇后问题的棋盘布局 这道题是一道作业题,我都惊到了,一向弱智的作业题中竟然冒出一道这样的题,这题最起码橙黄之 ...
- C语言中的操作符:了解与实践
欢迎大家来到贝蒂大讲堂 养成好习惯,先赞后看哦~ 所属专栏:C语言学习 贝蒂的主页:Betty's blog 1. 操作符的分类 操作符又叫运算符,它在C语言中起着非常大的作用,以下是 ...