百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术流 > 正文

C#托管堆 遭破坏 问题溯源分析(.net 托管)

citgpt 2024-09-07 13:31 11 浏览 0 评论

一:背景

1. 讲故事

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

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

C#托管堆 遭破坏 问题溯源分析(.net 托管)

二:抓取第一现场

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在床。

三:总结

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

相关推荐

js中arguments详解

一、简介了解arguments这个对象之前先来认识一下javascript的一些功能:其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数...

firewall-cmd 常用命令

目录firewalldzone说明firewallzone内容说明firewall-cmd常用参数firewall-cmd常用命令常用命令 回到顶部firewalldzone...

epel-release 是什么

EPEL-release(ExtraPackagesforEnterpriseLinux)是一个软件仓库,它为企业级Linux发行版(如CentOS、RHEL等)提供额外的软件包。以下是关于E...

FullGC详解  什么是 JVM 的 GC
FullGC详解 什么是 JVM 的 GC

前言:背景:一、什么是JVM的GC?JVM(JavaVirtualMachine)。JVM是Java程序的虚拟机,是一种实现Java语言的解...

2024-10-26 08:50 citgpt

使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
跨域(CrossOrigin)

1.介绍  1)跨域问题:跨域问题是在网络中,当一个网络的运行脚本(通常时JavaScript)试图访问另一个网络的资源时,如果这两个网络的端口、协议和域名不一致时就会出现跨域问题。    通俗讲...

微服务架构和分布式架构的区别

1、含义不同微服务架构:微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并...

深入理解与应用CSS clip-path 属性
深入理解与应用CSS clip-path 属性

clip-pathclip-path是什么clip-path 是一个CSS属性,允许开发者创建一个剪切区域,从而决定元素的哪些部分可见,哪些部分会被隐...

2024-10-25 11:51 citgpt

HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
Request.ServerVariables 大全

Request.ServerVariables("Url")返回服务器地址Request.ServerVariables("Path_Info")客户端提供的路...

python操作Kafka

目录一、python操作kafka1.python使用kafka生产者2.python使用kafka消费者3.使用docker中的kafka二、python操作kafka细...

Runtime.getRuntime().exec详解

Runtime.getRuntime().exec详解概述Runtime.getRuntime().exec用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。...

promise.all详解 promise.all是干什么的
promise.all详解 promise.all是干什么的

promise.all详解promise.all中所有的请求成功了,走.then(),在.then()中能得到一个数组,数组中是每个请求resolve抛出的结果...

2024-10-24 16:21 citgpt

Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解

取消回复欢迎 发表评论: