一:背景

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. html+css 面试题总结附答案

    行内元素有哪些? 块级元素有哪些? 块级元素:div p h1 ul li form table行内元素: a b br i span input select laber strong em img ...

  2. 深度剖析Java的volatile实现原理,再也不怕面试官问了

    上篇文章我们讲了synchronized的用法和实现原理,我们总爱说synchronized是重量级锁,volatile是轻量级锁.为什么volatile是轻量级锁,体现在哪些方面?以及volatil ...

  3. Unreal NetMode&NetRole 解析

    Version: Unreal 4.26 问题 为啥UE编辑器会有EPlayNetMode有三种让你选择. 为啥描述World 的ENetMode 会有4种,而不只是(Client/Server 2种 ...

  4. VM虚拟机搭建Linux CentOS7(手把手教程)

    VM虚拟机搭建Linux CentOS7(手把手教程) 目录 VM虚拟机搭建Linux CentOS7(手把手教程) 一.VM虚拟机和Linux镜像文件下载 1. 登录VM虚拟机官方地址: 2. 安装 ...

  5. 通过jmeter,造50个单位的并发用户数据(用户从上至下每50个单位依次排序)

    单位数据:loginfoll.csv         #50个单位的信息(agencyCode,passwd,agencyname,rgcode) 用户数据:50个单位,每个单位200个用户(用户名为 ...

  6. mindxdl---common--test_tools.go

    // Copyright (c) 2021. Huawei Technologies Co., Ltd. All rights reserved.// Package common define co ...

  7. Go语言核心36讲06

    我已经为你打开了Go语言编程之门,并向你展示了"程序从初建到拆分,再到模块化"的基本演化路径. 一个编程老手让程序完成基本演化,可能也就需要几十分钟甚至十几分钟,因为他们一开始就会 ...

  8. Android网络请求(3) 网络请求框架OkHttp

    Android网络请求(3) 网络请求框架OkHttp 本节我们来讲解OkHtpp网络请求框架 什么是网络请求框架 在我的理解中,网络请求框架是为了方便我们更加便捷规范的进行网络请求所建的类,我们通过 ...

  9. Vue使用Element表单校验错误Cannot read property ‘validate’ of undefined

    在做注册用户的页面使用表单校验一直提示Cannot read property 'validate' of undefined错误,其实这个错误的提示根据有多种情况,比较常见的就是 ref 的名字不一 ...

  10. 修改msi文件

    前言 msi文件是一个安装包文件,可以看做一个数据库,其中包含很多资源,例如图片,配置文件,可执行文件exe等等. 我想要把修改过 exe可执行文件提交到msi文件中,那么就需要知道msi文件的构成, ...