建萍 發表於 2023-1-29 10:51:00

C# 托管堆 遭破坏 问题溯源分析

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>年前遇到了好几例托管堆被损坏的案例,有些运气好一些,从被破坏的托管堆内存现场能观测出大概是什么问题,但更多的情况下是无法做出准确判断的,原因就在于生成的dump是第二现场,借用之前文章的一张图,大家可以理解一下。</p>
<p><img src="https://img2023.cnblogs.com/blog/214741/202301/214741-20230129105046704-188639668.png" alt="" loading="lazy"></p>
<p>为了帮助更多受此问题困扰的朋友,这篇来整理一下如何 <code>快狠准</code> 的抓取第一现场。</p>
<h2 id="二抓取第一现场">二:抓取第一现场</h2>
<h3 id="1-思路分析">1. 思路分析</h3>
<p>要想抓到第一现场,只需要让破坏托管堆的那个线程在修改完之后,回到 CLR Pinvoke 层的时候主动触发GC,因为这时候托管堆已经是损坏状态了,程序也就会立即崩溃,破坏线程也就被捉jian在床,画个图如下:</p>
<p><img src="https://img2023.cnblogs.com/blog/214741/202301/214741-20230129105046710-159750510.png" alt="" loading="lazy"></p>
<p>那如何让 <code>CLR:PInvoke</code> 主动触发GC呢? 这就需要借助微软的 <code>MDA</code> 托管调试助手,它有一个 <code>gcUnmanagedToManaged</code> 配置项就是专门做这件事情的,参考网址:https://learn.microsoft.com/zh-cn/dotnet/framework/debug-trace-profile/gcunmanagedtomanaged-mda</p>
<h3 id="2-如何配置-mda">2. 如何配置 MDA</h3>
<p>MDA 的配置非常简单,大体上分两步:</p>
<ol>
<li>提交注册表开启MDA</li>
</ol>
<p>这里使用注册表的方式,需要注意的是,程序和操作系统位数一致的话采用如下方式。</p>
<pre><code class="language-C#">
Windows Registry Editor Version 5.00


"MDA"="1"

</code></pre>
<p>如果不一致,采用如下配置,比如 32bit 程序跑在 64bit 系统上。</p>
<pre><code class="language-C#">
Windows Registry Editor Version 5.00


"MDA"="1"

</code></pre>
<p>这里我用的是第二段内容,按照官方文档描述,将内容保存到 <code>MDAEnable.reg</code> 中,然后在 <code>注册表编辑器</code> 上导入即可。</p>
<p><img src="https://img2023.cnblogs.com/blog/214741/202301/214741-20230129105046693-74317492.png" alt="" loading="lazy"></p>
<ol start="2">
<li>开启应用程序级捕获</li>
</ol>
<p>为了能够让 <code>gcUnmanagedToManaged</code> 生效,需要新建应用程序打头的配置文件,比如: <code>Example_16_1_2.exe.mda.config</code>,内容如下:</p>
<pre><code class="language-xml">
&lt;mdaConfig&gt;
        &lt;assistants&gt;
                &lt;gcUnmanagedToManaged/&gt;
        &lt;/assistants&gt;
&lt;/mdaConfig&gt;

</code></pre>
<p>完整截图:</p>
<p><img src="https://img2023.cnblogs.com/blog/214741/202301/214741-20230129105046726-1198803951.png" alt="" loading="lazy"></p>
<p>这样就算配置好了,当程序在 PInvoke 时,CLR 会读取注册表的 MDA 值,如果开启的话就会读取 <code>config</code> 中 <code>gcUnmanagedToManaged</code> 子节做相应的逻辑。</p>
<p>tips:如果配置不生效,保守一点的话,建议重启下机器。</p>
<h3 id="3-一个托管堆破坏的测试案例">3. 一个托管堆破坏的测试案例</h3>
<p>为了演示托管堆损坏,我准备将一个 string 传给 C++,然后让 C++ 溢出它来实现托管堆破坏。</p>
<p>C# 代码如下:</p>
<pre><code class="language-C#">
namespace Example_16_1_2
{
    internal class Program
    {
      
      public extern static void Alloc(string str);

      static void Main(string[] args)
      {
            Test();

            Task.Factory.StartNew(() =&gt;
            {
                Thread.Sleep(3000);
                GC.Collect();
            });

            Console.ReadLine();
      }

      static void Test()
      {
            var str = "hello";
            var str2 = "world!";

            Alloc(str);

      }
    }
}

</code></pre>
<p>C++ 代码如下:</p>
<pre><code class="language-C++">
extern "C"
{
        _declspec(dllexport) void Alloc(wchar_t* c);
}

#include "iostream"
#include &lt;Windows.h&gt;
using namespace std;

void Alloc(wchar_t* c)
{
        for (size_t i = 0; i &lt; 10; i++)
        {
                *c++ = 'a';
        }

        wprintf(L"%s \n", c);
}

</code></pre>
<p>从代码逻辑看,只要 <code>Alloc(str)</code> 的线程栈上触发了 GC 就是第一现场,Task 下的 <code>GC.Collect();</code> 是第二现场,如果是前者目的就达到了。</p>
<p>激动人心的时刻到了,把程序跑起来后,由于程序崩溃,procdump 立即给我抓了一个 crash dump,截图如下:</p>
<p><img src="https://img2023.cnblogs.com/blog/214741/202301/214741-20230129105046723-589422816.png" alt="" loading="lazy"></p>
<p>接下来打开 windbg,从序幕信息看果然是 GC 清扫的时候出的问题,托管堆也是损坏状态,信息如下:</p>
<pre><code class="language-C#">
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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00010206
clr!WKS::gc_heap::plan_phase+0x79b:
79a6f2d1 f70000000080    test    dword ptr ,80000000h ds:002b:00610060=????????

0:000&gt; !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.

</code></pre>
<p>那是不是<code>主线程</code>引发的GC呢?切过去便知。</p>
<pre><code class="language-C#">
0:000&gt; ~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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00010206
clr!WKS::gc_heap::plan_phase+0x79b:
79a6f2d1 f70000000080    test    dword ptr ,80000000h ds:002b:00610060=????????
0:000&gt; !clrstack
OS Thread Id: 0x52ac (0)
Child SP       IP Call Site
00d3f220 79a6f2d1 System.StubHelpers.StubHelpers.TriggerGCForMDA()
00d3f294 02bc0aa7 DomainBoundILStubClass.IL_STUB_PInvoke(System.String)
00d3f298 02bc09c9 Example_16_1_2.Program.Alloc(System.String)
00d3f2e0 02bc09c9 Example_16_1_2.Program.Test()
00d3f2f0 02bc0900 Example_16_1_2.Program.Main(System.String[])
00d3f490 7996f036
0:000&gt; 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
0a 00d3f318 7996f036   Example_16_1_2!Example_16_1_2.Program.Main+0x30
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

</code></pre>
<p>从线程栈上的 <code>clr!StubHelpers::TriggerGCForMDA</code> 来看,在 Pinvoke 层果然主动触发了 GC,成功将 <code>Program.Alloc</code> 这个非托管方法捉jian在床。</p>
<h2 id="三总结">三:总结</h2>
<p>在此之前很多朋友都会困惑于托管堆破坏导致的程序崩溃,希望这篇文章能够让后来者少走弯路。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/17072024.html
頁: [1]
查看完整版本: C# 托管堆 遭破坏 问题溯源分析