一:背景

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触发时会误认为这个无效的地址还是一个有效的对象,进而在标记阶段导致程序崩溃。

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

由于这个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某工控自动化系统 崩溃分析的更多相关文章

  1. 记一次 .NET 某工控软件 内存泄露分析

    一:背景 1.讲故事 上个月 .NET调试训练营 里的一位老朋友给我发了一个 8G 的dump文件,说他的程序内存泄露了,一时也没找出来是哪里的问题,让我帮忙看下到底是怎么回事,毕竟有了一些调试功底也 ...

  2. 记一次 .NET 某工控数据采集平台 线程数 爆高分析

    一:背景 1. 讲故事 前几天有位朋友在 B站 加到我,说他的程序出现了 线程数 爆高的问题,让我帮忙看一下怎么回事,截图如下: 说来也奇怪,这些天碰到了好几起关于线程数无缘无故的爆高,不过那几个问题 ...

  3. 记一次 .NET 某工控自动化控制系统 卡死分析

    一:背景 1. 讲故事 前段时间遇到了好几起关于窗体程序的 进程加载锁 引发的 程序卡死 和 线程暴涨 问题,这种 dump 分析难度较大,主要涉及到 Windows操作系统 和 C++ 的基础知识, ...

  4. 记一次 .NET 某工控视觉软件 非托管泄漏分析

    一:背景 1.讲故事 最近分享了好几篇关于 非托管内存泄漏 的文章,有时候就是这么神奇,来求助的都是这类型的dump,一饮一啄,莫非前定.让我被迫加深对 NT堆, 页堆 的理解,这一篇就给大家再带来一 ...

  5. 记一次 .NET 某工控MES程序 崩溃分析

    一:背景 1.讲故事 前几天有位朋友找到我,说他的程序出现了偶发性崩溃,已经抓到了dump文件,Windows事件日志显示的崩溃点在 clr.dll 中,让我帮忙看下是怎么回事,那到底怎么回事呢? 上 ...

  6. 记一次 .NET 某医疗器械 程序崩溃分析

    一:背景 1.讲故事 前段时间有位朋友在微信上找到我,说他的程序偶发性崩溃,让我帮忙看下怎么回事,上面给的压力比较大,对于这种偶发性崩溃,比较好的办法就是利用 AEDebug 在程序崩溃的时候自动抽一 ...

  7. 记一次 .NET 某医疗住院系统 崩溃分析

    一:背景 1. 讲故事 最近收到了两起程序崩溃的dump,查了下都是经典的 double free 造成的,蛮有意思,这里就抽一篇出来分享一下经验供后面的学习者避坑吧. 二:WinDbg 分析 1. ...

  8. Wireshark工控协议

    Wireshark是一个强大开源流量与协议分析工具,除了传统网络协议解码外,还支持众多主流和标准工控协议的分析与解码. 序号 协议类型 源码下载 简介 1 Siemens S7 https://git ...

  9. 开源纯C#工控网关+组态软件(十)移植到.NET Core

    一.   引子 写这个开源系列已经十来篇了.自从十年前注册博客园以来,关注了张善友.老赵.xiaotie.深蓝色右手等一众大牛,也围观了逗比的吉日嘎啦.精密顽石等形形色色的园友.然而整整十年一篇文章都 ...

  10. 【转】工控老鬼】西门子S7200入门&精通【1】S7200硬件大全

    转载地址:http://blog.sina.com.cn/s/blog_669692a601016i5f.html     工控老鬼提醒以下的信息和资料可能不全或者不准确,如有疑问可以查阅西门子中国网 ...

随机推荐

  1. 教你用Java7的Fork/Join框架开发高并发程序

    摘要:Fork/Join框架位于J.U.C(java.util.concurrent)中,是Java7中提供的用于执行并行任务的框架,其可以将大任务分割成若干个小任务,最终汇总每个小任务的结果后得到最 ...

  2. 提升数据决策时效,火山引擎DataLeapCDC分库分表能力升级!

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群   近日,大数据研发治理套件DataLeap数据集成更新CDC分库分表能力,可做到将多个实例的多个数据库的多个分表 ...

  3. Linux 创建新用户

    添加用户组[root@VipSoft ~]#groupadd admin 添加用户[root@VipSoft ~]#useradd jimmy -m -d /home/jimmy -g admin 修 ...

  4. 【主流技术】MongoTemplate 与 Spring Boot 项目集成分享(附CURD技巧)

    目录 前言 一.表结构特点 1.1Json格式 1.2实体映射 二.条件构造 2.1Criteria与Query的区别 2.2简单条件 2.3复杂条件 三.如何选用接口 3.1MongoReposit ...

  5. Android 编译线程爆了, gradle 内存 OOM 解决之路

    本文首发我的微信公众号徐公,收录于 Github·AndroidGuide,这里有 Android 进阶成长知识体系, 希望我们能够一起学习进步,关注公众号徐公,5 年中大厂程序员,一起建立核心竞争力 ...

  6. 【HZERO】消息发送

    消息发送 https://open.hand-china.com/community/detail/625843016338378752 新建模板 @Override public String sh ...

  7. 【scikit-learn基础】--『监督学习』之 谱聚类

    谱聚类算法基于图论,它的起源可以追溯到早期的图分割文献.不过,直至近年来,受益于计算机计算能力的提升,谱聚类算法才得到了广泛的研究和关注. 谱聚类被广泛应用于图像分割.社交网络分析.推荐系统.文本聚类 ...

  8. 【C++】使用 curl 库配置 HTTP 的 Post/Get 请求响应数据(封装一个简单类)

    2023.7.18 Update: [LibCurl]C++使用libcurl实现HTTP POST和GET 要想使用 LibCURL 库,首先需配置 CURL 库 参考链接:[C++开源库]Wind ...

  9. AtCoder Beginner Contest 195 Editorial

    AtCoder Beginner Contest 195 Editorial Problem A - Health M Death(opens new window) 只要检查 \(H\equiv 0 ...

  10. linux挂载磁盘和设置开机自动挂载

    1.查看分区信息 root@xmgl opt]# fdisk -l ......此处省略一些信息 Disk /dev/sdb: 644.2 GB, 644245094400 bytes, 125829 ...