一:背景

1. 讲故事

年前遇到了好几例托管堆被损坏的案例,有些运气好一些,从被破坏的托管堆内存现场能观测出大概是什么问题,但更多的情况下是无法做出准确判断的,原因就在于生成的dump是第二现场,借用之前文章的一张图,大家可以理解一下。

为了帮助更多受此问题困扰的朋友,这篇来整理一下如何 快狠准 的抓取第一现场。

二:抓取第一现场

1. 思路分析

要想抓到第一现场,只需要让破坏托管堆的那个线程在修改完之后,回到 CLR Pinvoke 层的时候主动触发GC,因为这时候托管堆已经是损坏状态了,程序也就会立即崩溃,破坏线程也就被捉jian在床,画个图如下:

那如何让 CLR:PInvoke 主动触发GC呢? 这就需要借助微软的 MDA 托管调试助手,它有一个 gcUnmanagedToManaged 配置项就是专门做这件事情的,参考网址:https://learn.microsoft.com/zh-cn/dotnet/framework/debug-trace-profile/gcunmanagedtomanaged-mda

2. 如何配置 MDA

MDA 的配置非常简单,大体上分两步:

  1. 提交注册表开启MDA

这里使用注册表的方式,需要注意的是,程序和操作系统位数一致的话采用如下方式。


Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework]
"MDA"="1"

如果不一致,采用如下配置,比如 32bit 程序跑在 64bit 系统上。


Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework]
"MDA"="1"

这里我用的是第二段内容,按照官方文档描述,将内容保存到 MDAEnable.reg 中,然后在 注册表编辑器 上导入即可。

  1. 开启应用程序级捕获

为了能够让 gcUnmanagedToManaged 生效,需要新建应用程序打头的配置文件,比如: Example_16_1_2.exe.mda.config,内容如下:


<mdaConfig>
<assistants>
<gcUnmanagedToManaged/>
</assistants>
</mdaConfig>

完整截图:

这样就算配置好了,当程序在 PInvoke 时,CLR 会读取注册表的 MDA 值,如果开启的话就会读取 configgcUnmanagedToManaged 子节做相应的逻辑。

tips:如果配置不生效,保守一点的话,建议重启下机器。

3. 一个托管堆破坏的测试案例

为了演示托管堆损坏,我准备将一个 string 传给 C++,然后让 C++ 溢出它来实现托管堆破坏。

C# 代码如下:


namespace Example_16_1_2
{
internal class Program
{
[DllImport("Example_16_1_3.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public extern static void Alloc(string str); static void Main(string[] args)
{
Test(); Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
GC.Collect();
}); Console.ReadLine();
} static void Test()
{
var str = "hello";
var str2 = "world!"; Alloc(str); }
}
}

C++ 代码如下:


extern "C"
{
_declspec(dllexport) void Alloc(wchar_t* c);
} #include "iostream"
#include <Windows.h>
using namespace std; void Alloc(wchar_t* c)
{
for (size_t i = 0; i < 10; i++)
{
*c++ = 'a';
} wprintf(L"%s \n", c);
}

从代码逻辑看,只要 Alloc(str) 的线程栈上触发了 GC 就是第一现场,Task 下的 GC.Collect(); 是第二现场,如果是前者目的就达到了。

激动人心的时刻到了,把程序跑起来后,由于程序崩溃,procdump 立即给我抓了一个 crash dump,截图如下:

接下来打开 windbg,从序幕信息看果然是 GC 清扫的时候出的问题,托管堆也是损坏状态,信息如下:


Debug session time: Sun Jan 29 10:14:21.000 2023 (UTC + 8:00)
System Uptime: 0 days 1:14:11.423
Process Uptime: not available
.................................
Loading unloaded module list
..
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(4460.52ac): Access violation - code c0000005 (first/second chance not available)
For analysis of this file, run !analyze -v
eax=00610060 ebx=00000000 ecx=02da23a4 edx=00000001 esi=02da2370 edi=02da2388
eip=79a6f2d1 esp=00d3ef64 ebp=00d3f104 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::plan_phase+0x79b:
79a6f2d1 f70000000080 test dword ptr [eax],80000000h ds:002b:00610060=???????? 0:000> !VerifyHeap
Could not request method table data for object 02DA1228 (MethodTable: 0000000C).
Last good object: 02DA121C.
object 03da1020: bad member 02DA1228 at 03DA1098
Last good object: 03DA1010.
object 03da2338: bad member 02DA1228 at 03DA2340
Last good object: 03DA2328.
object 03da3568: bad member 02DA2364 at 03DA357C
Last good object: 03DA3558.
Failed to request SyncBlk at index 1.

那是不是主线程引发的GC呢?切过去便知。


0:000> ~0s
eax=00610060 ebx=00000000 ecx=02da23a4 edx=00000001 esi=02da2370 edi=02da2388
eip=79a6f2d1 esp=00d3ef64 ebp=00d3f104 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::plan_phase+0x79b:
79a6f2d1 f70000000080 test dword ptr [eax],80000000h ds:002b:00610060=????????
0:000> !clrstack
OS Thread Id: 0x52ac (0)
Child SP IP Call Site
00d3f220 79a6f2d1 [HelperMethodFrame: 00d3f220] System.StubHelpers.StubHelpers.TriggerGCForMDA()
00d3f294 02bc0aa7 DomainBoundILStubClass.IL_STUB_PInvoke(System.String)
00d3f298 02bc09c9 [InlinedCallFrame: 00d3f298] Example_16_1_2.Program.Alloc(System.String)
00d3f2e0 02bc09c9 Example_16_1_2.Program.Test() [D:\testdump\Example\Example_16_1_2\Program.cs @ 35]
00d3f2f0 02bc0900 Example_16_1_2.Program.Main(System.String[]) [D:\testdump\Example\Example_16_1_2\Program.cs @ 19]
00d3f490 7996f036 [GCFrame: 00d3f490]
0:000> k 10
# ChildEBP RetAddr
00 00d3f104 79a68153 clr!WKS::gc_heap::plan_phase+0x79b
01 00d3f124 79a6847b clr!WKS::gc_heap::gc1+0xbc
02 00d3f13c 79a68585 clr!WKS::gc_heap::garbage_collect+0x367
03 00d3f15c 79b1ddbd clr!WKS::GCHeap::GarbageCollectGeneration+0x1bd
04 00d3f16c 79b1de34 clr!WKS::GCHeap::GarbageCollectTry+0x71
05 00d3f198 79d20aed clr!WKS::GCHeap::GarbageCollect+0xac
06 00d3f204 79d066c0 clr!TriggerGCForMDAInternal+0x7d
07 00d3f28c 02bc0aa7 clr!StubHelpers::TriggerGCForMDA+0x61
WARNING: Frame IP not in any known module. Following frames may be wrong.
08 00d3f2d8 02bc09c9 0x2bc0aa7
09 00d3f2e8 02bc0900 Example_16_1_2!Example_16_1_2.Program.Test+0x39 [D:\testdump\Example\Example_16_1_2\Program.cs @ 35]
0a 00d3f318 7996f036 Example_16_1_2!Example_16_1_2.Program.Main+0x30 [D:\testdump\Example\Example_16_1_2\Program.cs @ 19]
0b 00d3f324 799722da clr!CallDescrWorkerInternal+0x34
0c 00d3f378 7997859b clr!CallDescrWorkerWithHandler+0x6b
0d 00d3f3ec 79b1b11b clr!MethodDescCallSite::CallTargetWorker+0x16a
0e 00d3f510 79b1b7fa clr!RunMain+0x1b3
0f 00d3f77c 79b1b727 clr!Assembly::ExecuteMainMethod+0xf7

从线程栈上的 clr!StubHelpers::TriggerGCForMDA 来看,在 Pinvoke 层果然主动触发了 GC,成功将 Program.Alloc 这个非托管方法捉jian在床。

三:总结

在此之前很多朋友都会困惑于托管堆破坏导致的程序崩溃,希望这篇文章能够让后来者少走弯路。

C# 托管堆 遭破坏 问题溯源分析的更多相关文章

  1. C#中堆和栈的区别分析

    线程堆栈:简称栈 Stack托管堆: 简称堆 Heap 使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切.如果我们写出如下两段代码: 1 代码段1: 2 3 ...

  2. C#中堆和栈的区别分析(有待更新总结2)

    转载:http://blog.csdn.net/Zevin/article/details/5731965 线程堆栈:简称栈 Stack 托管堆: 简称堆 Heap 使用.Net框架开发程序的时候,我 ...

  3. 重温CLR(十五) 托管堆和垃圾回收

    本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计 ...

  4. cir from c# 托管堆和垃圾回收

    1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ...

  5. 【C#进阶系列】21 托管堆和垃圾回收

    托管堆基础 一般创建一个对象就是通过调用IL指令newobj分配内存,然后初始化内存,也就是实例构造器时做这个事. 然后在使用完对象后,摧毁资源的状态以进行清理,然后由垃圾回收器来释放内存. 托管堆除 ...

  6. C#中的托管堆和堆栈

    托管堆(Heap)和堆栈(Stack)是内存的逻辑划分.   栈 堆 连续性 连续 不连续 有序性 后进先出 无序 内存管理 操作系统自动释放 GC或人工 存放类型 值类型/引用 引用类型 注:内存格 ...

  7. C#堆栈和托管堆

    首先堆栈和堆(托管堆)都在进程的虚拟内存中.(在32位处理器上每个进程的虚拟内存为4GB) 堆栈stack 堆栈中存储值类型. 堆栈实际上是向下填充,即由高内存地址指向低内存地址填充. 堆栈的工作方式 ...

  8. .NET 托管堆和垃圾回收

       托管堆基础 简述:每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.....事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源.要使用这些资源,必须为代表 ...

  9. 栈和托管堆/值类型和引用类型/强制类型转换/装箱和拆箱[C#]

    原文地址:http://www.cnblogs.com/xy8.cn/articles/1227228.html 一.栈和托管堆      通用类型系统(CTS)区分两种基本类型:值类型和引用类型.它 ...

  10. .Net之托管堆资源分配

    托管堆分配资源: 一:进程初始化是,CLR要保留一块联系的地址空间,这个地址空间最初并没有对应的物理存储空间.这个地址空间就是托管堆.托管堆还维护着一个指针,我把它称为NextObjPtr.它指向下个 ...

随机推荐

  1. C# Linq 查询汇总

    分组取值.求和.计数 1 var resultlist = orderllist.GroupBy(oo => new { oo.Deptname, oo.Userid, oo.Username ...

  2. Python基础之模块:7、项目开发流程和项目需求分析及软件开发目录

    一.项目开发流程 1.项目需求分析 明确项目具体功能: 明确到底要写什么东西,实现什么功能,在这个阶段的具体要询问项目经理和客户的需求 参与人员: 产品经理.架构师.开发经理 技术人员主要职责: 引导 ...

  3. 【JVM】关于JVM,你需要掌握这些 | 一文彻底吃透JVM系列

    写在前面 最近,一直有小伙伴让我整理下关于JVM的知识,经过十几天的收集与整理,初版算是整理出来了.希望对大家有所帮助. JDK 是什么? JDK 是用于支持 Java 程序开发的最小环境. Java ...

  4. CF240F (26颗线段树计数)

    题目链接:Topcoder----洛谷 题目大意: 给定一个长为n的由a到z组成的字符串,有m次操作,每次操作将[l,r]这些位置的字符进行重排,得到字典序最小的回文字符串,如果无法操作就不进行. 思 ...

  5. Nginx负载均衡策略的介绍与调优

    工作中经常会用到nginx负载均衡这一块,下面对nginx负载均衡策略做个总结.本人在工作中最常用到的负载均衡策略是轮询策略. 在一般情况下,Web中间件最大的作用就是负责对请求进行分发,也就是我们常 ...

  6. 【实操日记】使用 PyQt5 设计下载远程服务器日志文件程序

    最近通过 PyQt5 设计了一个下载服务器指定日期日志文件的程序,里面有些有意思的技术点,现在做一些分享. PyQt5 是一套 Python 绑定 Digia Qt5 应用的框架,是最强大的 GUI ...

  7. vue3中使用computed

    演示示例(vant组件库的轮播图): <van-swipe :loop="false" :width="150" class="my-Swipe ...

  8. 支持JDK19虚拟线程的web框架,之五(终篇):兴风作浪的ThreadLocal

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos <支持JDK19虚拟线程的web框架>系列 ...

  9. 嵌入式-C语言基础:二级指针

    二级指针:可以理解为指向指针的指针,存放的是指针变量的地址. 下面用一级指针来保存一个指针变量的地址: #include<stdio.h> int main() { int *p1; in ...

  10. 小米mini路由器刷breed不死鸟和潘多拉固件

    前言 开启小米路由器ssh, 这一步浪费我很长时间,因为目前的开发版都对ssh升级进行了md5校验,导致官方升级方法总是失败,所以换成老版本的 路由器固件就行了. 步骤 下载 0.4.36 mini路 ...