Net 高级调试之十三:托管堆的几个经典破坏问题

一、介绍
      今天是《Net 高级调试》的第十三篇文章,这篇文章写作时间的跨度有点长。这篇文章我们主要介绍 经典的案例,如何查找问题,如何解决问题,最重要我们看到了问题,要有解决的思路,没有思路就是死路一条了,当然,这个过程也不是一帆风顺的,我是做了很多遍,最终猜得到了想要的东西。当然了,第一次看视频或者看书,是很迷糊的,不知道如何操作,还是那句老话,一遍不行,那就再来一遍,还不行,那就再来一遍,俗话说的好,书读千遍,其意自现。
      如果在没有说明的情况下,所有代码的测试环境都是 Net Framewok 4.8,但是,有时候为了查看源码,可能需要使用 Net Core 的项目,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
         操作系统:Windows Professional 10
**         调试工具:Windbg Preview(可以去Microsoft Store 去下载)**
**         开发工具:Visual Studio 2022**
**         Net 版本:Net Framework 4.8**
**         CoreCLR源码:源码下载**
二、基础知识
      1、托管堆损坏
          1.1、简介
              在高级调试的过程中,经常会遇到这种情况,程序会出现各种莫名其妙的崩溃,查看 dump 文件大多是"访问违例",比如:访问了只读内存;地址超出了内存表示范围;访问了 0 区,即空指针。这一篇来说一个"托管堆"被损坏的场景,可能 有很多人会有疑问,托管堆会损坏,这里所说的损坏是指把托管堆上的地址传给了"非托管代码",比如:c++,而后者在操作时越界造成的。

1.2、 MDA排查 排查方式
               可以在应用程序的 MDA 配置文件中单独地启用、禁用和配置某些助手。 若要使用用于配置 MDA 的应用程序配置文件,必须设置 MDA 注册表项或 COMPLUS_MDA 环境变量。 应用程序配置文件通常与应用程序的可执行文件 (.exe) 位于同一目录中。 文件名采用的格式为 ApplicationName.mda.config;例如,notepad.exe.mda.config。在应用程序配置文件中启用的助手可能具有设计用于控制该助手行为的属性或元素。
               如果想查看原文:https://learn.microsoft.com/zh-cn/dotnet/framework/debug-trace-profile/diagnosing-errors-with-managed-debugging-assistants,这里面内容很详细。
               如果想要 MDA 起作用,要仔细的看文档,说白了就是一个道理,无论是从托管到非托管,还是从非托管到托管,只要有这样的切换,我们就启动 GC,GC 进行垃圾回收,要识别托管堆的完整性,如果托管堆被破坏,就可以及时抛出异常,让我们査知。
              
      2、托管堆碎片化
          当我们使用 Windbg 分析 dump 的过程中,会发现有很多 free 块的情况,这就是托管堆碎片化,而且占据空间比较大,这个时候就要考虑出现这种情况的原因。托管堆碎片化大多是由 Free 块前后的对象由于被某种 handle 所持有,导致 GC 不能很好的合并 Free 块。

3、非托管泄露
          在 Net 高级调试 的这本书中演示的"XmlSerializer"造成的非托管内存泄漏,我们这里说一下如何去发现这种内存泄漏,我们使用两种工具排查。一种是 Windebug,可以从动态 Module 中发现其中很多的类型。另外一种是使用 PerfView,这个工具可以捕获到底是谁分配的"程序集",使用 ETW 的程序集加载事件,并记录调用栈。
          当然,我们想要调试,必须现有调试工具,Windbg可以在 Microsoft Store(微软商店)里直接下载,很方便,能下载最新的版本,这里就不多说了。
          PerfView 这个工具可以通过微软的 bing.com 查找下载,是绿色软件,不需要安装,就可以直接使用,我使用的版本是:3.0.7。
          下载地址:https://github.com/microsoft/perfview/releases

三、调试过程
      废话不多说,这一节是具体的调试过程,又可以说是眼见为实的过程,在开始之前,我还是要啰嗦两句,这一节分为两个部分,第一部分是测试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。第二部分就是根据具体的代码来证实我们学到的知识,是具体的眼见为实。

1、调试源码
          1.1、Example_13_1_1
              第一部分:代码分成两个部分,第一部分是 C# 代码,Program.cs 源码如下:

复制代码
 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 namespace Example_13_1_1
 5 {
 6     internal class Program
 7     {
 8         [DllImport("Example_13_1_1_2.dll",CallingConvention =CallingConvention.Cdecl,CharSet =CharSet.Unicode)]
 9         public extern static int InitChars(char[] chars);
10 
11         static void Main(string[] args)
12         {
13             char[] c = { 'a', 'b', 'c'};
14             var len = InitChars(c);
15 
16             Console.WriteLine($"len={len}");
17             Console.Read();
18         }
19     }
20 }

View Code

第二部分,我还使用 Visual Studio 2022 创建了一个 C++ 项目,项目名:Example_13_1_1_2,Example_13_1_1_2.cpp 源码如下:

复制代码
 1 // Example_13_1_1_2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
 2 //
 3 
 4 extern "C"
 5 {
 6     _declspec(dllexport) int InitChars(wchar_t c[]);
 7 }
 8 
 9 #include <iostream>
10 using namespace std;
11 
12 int InitChars(wchar_t* c)
13 {
14     for (size_t i = 0; i < 100; i++)
15     {
16         c[i] = L'a';
17     }
18     return sizeof(wchar_t) * 100;
19 }
20 
21 //int InitChars(wchar_t c[])
22 //{
23 //    for (size_t i = 0; i < 100; i++)
24 //    {
25 //        c[i] = L'a';
26 //    }
27 //    return sizeof(wchar_t) * 100;
28 //}

View Code
              C++ 的项目需要说明一下,项目的属性【配置类型】是"动态库(.dll)",输出目录是 Example_13_1_1 项目的 bin\Debug 目录。
              在这个项目里还有一个 mda 的配置文件,Example_13_1_1.exe.mda.config,源码如下:

复制代码
1 <?xml version="1.0" encoding="utf-8" ?>
2 <mdaConfig>
3     <assistants>
4         <gcManagedToUnmanaged/>
5         <gcUnmanagedToManaged/>
6     </assistants>
7 </mdaConfig>

1.2、Example_13_1_2

复制代码
 1 using System.Diagnostics;
 2 
 3 namespace Example_13_1_2
 4 {
 5     internal class Program
 6     {
 7         public static List<byte[]?> list = new List<byte[]?>();
 8 
 9         static void Main(string[] args)
10         {
11             Alloc();
12 
13             Console.WriteLine("2G 数据分配完毕,请观察");
14             Console.ReadLine();
15 
16             GC.Collect();
17             Console.WriteLine("碎片化已经产生,请观察");
18 
19             Debugger.Break();
20         }
21 
22         private static void Alloc()
23         {
24             for (int i = 0; i < 1000; i++)
25             {
26                 var byteLength = 1024 * 1024 * (i % 2 == 0 ? 2 : 1);
27                 list.Add(new byte[byteLength]);
28 
29                 if (i % 2 == 0)
30                 {
31                     list[i] = null;
32                 }
33             }
34         }
35     }
36 }

View Code

1.3、Example_13_1_3

复制代码
 1 using System.Linq;
 2 using System;
 3 using System.IO;
 4 using System.Xml.Serialization;
 5 using System.Xml;
 6 
 7 namespace Example_13_1_3
 8 {
 9     internal class Program
10     {
11         static void Main(string[] args)
12         {
13             var xml = @"<FabrikamCustomer><Id>001</Id><FirstName>John</FirstName><LastName>Dow</LastName></FabrikamCustomer>";
14 
15             Enumerable.Range(0, 30000)
16                 .Select(i => GetCustomer(i, "FabrikamCustomer", xml))
17                 .ToList();
18 
19             Console.WriteLine("处理完成!");
20             Console.ReadLine();
21         }
22 
23         public static Customer GetCustomer(int i, string rootElementName, string xml)
24         {
25             var xmlSerializer = new XmlSerializer(typeof(Customer), new XmlRootAttribute(rootElementName));
26             using (var textReader = new StringReader(xml))
27             {
28                 using (var xmlReader = XmlReader.Create(textReader))
29                 {
30                     Console.WriteLine(i);
31 
32                     return (Customer)xmlSerializer.Deserialize(xmlReader);
33                 }
34             }
35         }
36     }
37 
38     public class Customer
39     {
40         public string Id { get; set; }
41         public string FirstName { get; set; }
42         public string LastName { get; set; }
43     }
44 }

View Code
 
      2、眼见为实
        项目的所有操作都是一样的,所以就在这里说明一下,但是每个测试例子,都需要重新启动,并加载相应的应用程序,加载方法都是一样的。流程如下:我们编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。**          2.1、启动 MDA 来处理托管和非托管交互处理过程的问题。
              调试源码:Example_13_1_1(c# 源码),Example_13_1_1_2(C++源码)** 想要启动 MDA,需要提前做一些准备,我在"基础知识"中,贴出了网址,如果不熟悉的大家可以自己恶补。我的操作过程是通过三步完成的,第一步,配置环境变量,第二步:配置项目的 MDA 配置文件,第三步就可以执行测试了。****              第一步:我在我的电脑里配置环境变量。COMPLUS_MDA=1,启动 MDA.
             如图:
              ****

**第二步:我为我的程序配置了 mda 配置文件,文件名:Example_13_1_1.exe.mda.config。**配置详情如下:

复制代码
1 <?xml version="1.0" encoding="utf-8" ?>
2 <mdaConfig>
3     <assistants>
4         <gcManagedToUnmanaged/>
5         <gcUnmanagedToManaged/>
6     </assistants>
7 </mdaConfig>

第三步:我没有选择调试工具,直接运行 exe,你就能看到有错误了,否则,错误很难发现的。 效果如图:

其实,只要我们启动了 MDA,使用 Visual Studio 也是可以检测的,VS就会及时抛出异常,如图:

以上是解决问题的方法,等程序出错,能够及时通知我们。

接下来,我们通过 Windbg 查看一下,做到眼见为实,看看内存被修改是什么样子。

我们需要使用【g】命令,继续运行程序,我们可以看到 Windbg 捕获到了异常。

复制代码
 1 0:000> g
 2 ModLoad: 00007ffb`7ed00000 00007ffb`7edaa000   C:\Windows\System32\ADVAPI32.dll
 3 ModLoad: 00007ffb`7f020000 00007ffb`7f0be000   C:\Windows\System32\msvcrt.dll
 4 ModLoad: 00007ffb`7d9d0000 00007ffb`7da6b000   C:\Windows\System32\sechost.dll
 5 ModLoad: 00007ffb`7ee10000 00007ffb`7ef33000   C:\Windows\System32\RPCRT4.dll
 6 ModLoad: 00007ffb`65540000 00007ffb`655ea000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscoreei.dll
 7 ModLoad: 00007ffb`7f3f0000 00007ffb`7f445000   C:\Windows\System32\SHLWAPI.dll
 8 ModLoad: 00007ffb`7ca70000 00007ffb`7ca83000   C:\Windows\System32\kernel.appcore.dll
 9 ModLoad: 00007ffb`7c460000 00007ffb`7c46a000   C:\Windows\SYSTEM32\VERSION.dll
10 ModLoad: 00007ffb`5eeb0000 00007ffb`5f972000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
11 ModLoad: 00007ffb`7db20000 00007ffb`7dcc0000   C:\Windows\System32\USER32.dll
12 ModLoad: 00007ffb`7cb70000 00007ffb`7cb92000   C:\Windows\System32\win32u.dll
13 ModLoad: 00007ffb`66830000 00007ffb`66846000   C:\Windows\SYSTEM32\VCRUNTIME140_CLR0400.dll
14 ModLoad: 00007ffb`5e9b0000 00007ffb`5ea6d000   C:\Windows\SYSTEM32\ucrtbase_clr0400.dll
15 ModLoad: 00007ffb`7d540000 00007ffb`7d56a000   C:\Windows\System32\GDI32.dll
16 ModLoad: 00007ffb`7cc60000 00007ffb`7cd6a000   C:\Windows\System32\gdi32full.dll
17 ModLoad: 00007ffb`7cd70000 00007ffb`7ce0d000   C:\Windows\System32\msvcp_win.dll
18 ModLoad: 00007ffb`7ce10000 00007ffb`7cf10000   C:\Windows\System32\ucrtbase.dll
19 ModLoad: 00007ffb`7e400000 00007ffb`7e430000   C:\Windows\System32\IMM32.DLL
20 ModLoad: 00007ffb`7d570000 00007ffb`7d8c4000   C:\Windows\System32\combase.dll
21 (3694.300c): Unknown exception - code 04242420 (first chance)
22 ModLoad: 00007ffb`5be50000 00007ffb`5d450000   C:\Windows\assembly\\mscorlib.ni.dll
23 ModLoad: 00007ffb`7dcc0000 00007ffb`7dde9000   C:\Windows\System32\ole32.dll
24 ModLoad: 00007ffb`7d570000 00007ffb`7d8c4000   C:\Windows\System32\combase.dll
25 ModLoad: 00007ffb`7c9f0000 00007ffb`7ca6f000   C:\Windows\System32\bcryptPrimitives.dll
26 ModLoad: 00007ffb`5b680000 00007ffb`5b7cf000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clrjit.dll
27 ModLoad: 00007ffb`6b170000 00007ffb`6b195000   E:\Visual Studio 2022\Example_13_1_1\bin\Debug\Example_13_1_1_2.dll
28 ModLoad: 00007ffb`6b140000 00007ffb`6b16e000   C:\Windows\SYSTEM32\VCRUNTIME140D.dll
29 ModLoad: 00007ffb`0ffa0000 00007ffb`101bf000   C:\Windows\SYSTEM32\ucrtbased.dll
30 (3694.300c): Access violation - code c0000005 (first chance)
31 First chance exceptions are reported before any exception handling.
32 This exception may be expected and handled.33 clr!WKS::gc_heap::mark_phase+0x73:
34 00007ffb`5ef1bd4d 393b            cmp     dword ptr [rbx],edi ds:00610061`00610060=????????

红色标注的就是发生了异常。发生了异常,我们看看当前线程的线程栈。

复制代码
 1 0:000> !clrstack -l
 2 OS Thread Id: 0x300c (0)
 3         Child SP               IP Call Site
 4 00000011dbb6eda8 00007ffb5ef1bd4d [HelperMethodFrame: 00000011dbb6eda8] System.StubHelpers.StubHelpers.TriggerGCForMDA()
 5 00000011dbb6eeb8 00007ffaff970ad6 [InlinedCallFrame: 00000011dbb6eeb8] Example_13_1_1.Program.InitChars(Char[])
 6 00000011dbb6ee90 00007ffaff970ad6 DomainBoundILStubClass.IL_STUB_PInvoke(Char[])
 7 
 8 00000011dbb6ef90 00007ffaff97090b Example_13_1_1.Program.Main(System.String[]) [E:\Visual Studio 2022\Example_13_1_1\Program.cs @ 14]
 9     LOCALS:
10         0x00000011dbb6efd0 = 0x000001fb00002ed0
11         0x00000011dbb6efec = 0x0000000000000000
12 
13 00000011dbb6f1f8 00007ffb5eeb6913 [GCFrame: 00000011dbb6f1f8] 

红色标记的地址"0x000001fb00002ed0"就是char[],我们可以使用【!dumpobj /d 0x000001fb00002ed0】命令确认一下。

复制代码
1 0:000> !dumpobj /d 0x000001fb00002ed0
2 Name:        System.Char\[\] 这就是我们的数组,我们可以使用dp 命令查看他的内容。3 MethodTable: 00007ffb5be767d0
4 EEClass:     00007ffb5bfe5870
5 Size:        30(0x1e) bytes
6 Array:       Rank 1, Number of elements 3, Type Char (Print Array)
7 Content:     aaa
8 Fields:
9 None

我们使用【dp 000001fb00002ed0】命令,查看一下,0061 就是 a ,这么多 a 就是被 C++ 该写的。

复制代码
1 0:000> dp 000001fb00002ed0
2 000001fb`00002ed0  00007ffb`5be767d0 00000000`00000003
3 000001fb`00002ee0  00610061`00610061 00610061`00610061
4 000001fb`00002ef0  00610061`00610061 00610061`00610061
5 000001fb`00002f00  00610061`00610061 00610061`00610061
6 000001fb`00002f10  00610061`00610061 00610061`00610061
7 000001fb`00002f20  00610061`00610061 00610061`00610061
8 000001fb`00002f30  00610061`00610061 00610061`00610061
9 000001fb`00002f40  00610061`00610061 00610061`00610061

我们也可以使用【?】查看一下 0061 的值,是十进制的 97,97对应的字母就是 a。

复制代码
1 0:000> ? 0061
2 Evaluate expression: 97 = 00000000`00000061

00007ffb`5be767d0 这个地址就是方发表。我们可以使用【!dumpmt 】命令查看一下。

复制代码
 1 0:000> !dumpmt 00007ffb`5be767d0
 2 EEClass:         00007ffb5bfe5870
 3 Module:          00007ffb5be51000
 4 Name:            System.Char[]
 5 mdToken:         0000000002000000
 6 File:            C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
 7 BaseSize:        0x18
 8 ComponentSize:   0x2
 9 Slots in VTable: 28
10 Number of IFaces in IFaceMap: 6

00000000`00000003 这个值就是数组的长度,char[] c = { 'a', 'b', 'c'},这个数组有3个元素。我们应该只有3个 00610061`00610061,但是这里却有很多,因为被 C++ 程序覆写了。这个测试程序虽然没有出错,可能是GC 还不知道,或者还没有被其他使用,如果一有使用,就会有想不到的错误,如果我们不使用 MDA,这种错误是很难发现的。
**          2.2、我们来解决托管堆碎片化的问题。
              调试源码:Example_13_1_2(这个程序是 Net 7.0,不是Net framework版本的,托管堆会不一样。)** 当我们进入调试器界面,调试器是处于 int 3 中断状态的,我们需要使用【g】命令,继续运行程序,我们的程序会输出:2G 数据分配完毕,请观察。因为是大对象,我们先查看一下托管堆,主要是关注大对象堆。我们点击【break】按钮暂停,开始调试我们的程序。

复制代码
 1 0:001> !eeheap -gc
 2 
 3 ========================================
 4 Number of GC Heaps: 1
 5 ----------------------------------------
 6 Small object heap
 7          segment            begin        allocated        committed allocated size       committed size      
 8 generation 0:
 9     02cbf425f320     028be2c00020     028be2c0e1f0     028be2c11000 0xe1d0 (57808)       0x11000 (69632)     
10 generation 1:
11     02cbf425f1c0     028be2400020     028be2409fb8     028be2411000 0x9f98 (40856)       0x11000 (69632)     
12 generation 2:
13     02cbf425f950     028be5000020     028be5000020     028be5001000                      0x1000 (4096)       
14 Large object heap
15          segment            begin        allocated        committed allocated size       committed size      
16     02cbf425f3d0     028be3000020     028be4f004b8     028be4f01000 0x1f00498 (32507032) 0x1f01000 (32509952)
17     02cbf425fa00     028be5400020     028be7300480     028be7301000 0x1f00460 (32506976) 0x1f01000 (32509952)
18     02cbf425ff80     028be7400020     028be93004b8     028be9301000 0x1f00498 (32507032) 0x1f01000 (32509952)
19     02cbf4260500     028be9400020     028beb100448     028beb121000 0x1d00428 (30409768) 0x1d21000 (30543872)
20     02cbf4260a80     028beb400020     028bed3004b8     028bed301000 0x1f00498 (32507032) 0x1f01000 (32509952)
21     02cbf4261000     028bed400020     028bef3004b8     028bef301000 0x1f00498 (32507032) 0x1f01000 (32509952)
22     02cbf4261580     028bef400020     028bf13004b8     028bf1301000 0x1f00498 (32507032) 0x1f01000 (32509952)
23     02cbf4261b00     028bf1400020     028bf33004b8     028bf3301000 0x1f00498 (32507032) 0x1f01000 (32509952)
24     02cbf4262080     028bf3400020     028bf53004b8     028bf5301000 0x1f00498 (32507032) 0x1f01000 (32509952)
25     02cbf4262600     028bf5400020     028bf7100448     028bf7121000 0x1d00428 (30409768) 0x1d21000 (30543872)
26     02cbf4262b80     028bf7400020     028bf93004b8     028bf9301000 0x1f00498 (32507032) 0x1f01000 (32509952)
27     02cbf4263100     028bf9400020     028bfb3004b8     028bfb301000 0x1f00498 (32507032) 0x1f01000 (32509952)
28     02cbf4263680     028bfb400020     028bfd100448     028bfd121000 0x1d00428 (30409768) 0x1d21000 (30543872)
29     02cbf4263c00     028bfd400020     028bff3004b8     028bff301000 0x1f00498 (32507032) 0x1f01000 (32509952)
30     02cbf4264180     028bff400020     028c013004b8     028c01301000 0x1f00498 (32507032) 0x1f01000 (32509952)
31     02cbf4264700     028c01400020     028c03100448     028c03121000 0x1d00428 (30409768) 0x1d21000 (30543872)
32     02cbf4264c80     028c03400020     028c053004b8     028c05301000 0x1f00498 (32507032) 0x1f01000 (32509952)
33     02cbf4265200     028c05400020     028c073004b8     028c07301000 0x1f00498 (32507032) 0x1f01000 (32509952)
34     02cbf4265780     028c07400020     028c09100448     028c09121000 0x1d00428 (30409768) 0x1d21000 (30543872)
35     02cbf4265d00     028c09400020     028c0b3004b8     028c0b301000 0x1f00498 (32507032) 0x1f01000 (32509952)
36     02cbf4266280     028c0b400020     028c0d3004b8     028c0d301000 0x1f00498 (32507032) 0x1f01000 (32509952)
37     02cbf4266800     028c0d400020     028c0f100448     028c0f121000 0x1d00428 (30409768) 0x1d21000 (30543872)
38     02cbf4266d80     028c0f400020     028c113004b8     028c11301000 0x1f00498 (32507032) 0x1f01000 (32509952)
39     02cbf4267300     028c11400020     028c133004b8     028c13301000 0x1f00498 (32507032) 0x1f01000 (32509952)
40     02cbf4267880     028c13400020     028c15100448     028c15121000 0x1d00428 (30409768) 0x1d21000 (30543872)
41     02cbf4267e00     028c15400020     028c173004b8     028c17301000 0x1f00498 (32507032) 0x1f01000 (32509952)
42     02cbf4268380     028c17400020     028c193004b8     028c19301000 0x1f00498 (32507032) 0x1f01000 (32509952)
43     02cbf4268900     028c19400020     028c1b100448     028c1b121000 0x1d00428 (30409768) 0x1d21000 (30543872)
44     02cbf4268e80     028c1b400020     028c1c7002f8     028c1c721000 0x13002d8 (19923672) 0x1321000 (20058112)
45 Pinned object heap
46          segment            begin        allocated        committed allocated size       committed size      
47     02cbf425ec40     028be0400020     028be0404428     028be0411000 0x4408 (17416)       0x11000 (69632)     
48 ------------------------------
49 GC Allocated Heap Size:    Size: 0x36724530 (913458480) bytes.
50 GC Committed Heap Size:    Size: 0x36871000 (914821120) bytes.

我们可以看到,大对象堆有很多东西。我们随便找一个 Segment 查看一下具体的情况。我选择最后一个,红色标注的地址范围。

复制代码
 1 0:001> !dumpheap 028c1b400020     028c1c7002f8
 2          Address               MT           Size
 3     028c1b400020     028bde5b2910             32 Free
 4     028c1b400040     7ffa9f2ac4a0      1,048,600 
 5     028c1b500058     028bde5b2910      2,097,240 Free
 6     028c1b7000b0     7ffa9f2ac4a0      1,048,600 
 7     028c1b8000c8     028bde5b2910      2,097,240 Free
 8     028c1ba00120     7ffa9f2ac4a0      1,048,600 
 9     028c1bb00138     028bde5b2910      2,097,240 Free
10     028c1bd00190     7ffa9f2ac4a0      1,048,600 
11     028c1be001a8     028bde5b2910      2,097,240 Free
12     028c1c000200     7ffa9f2ac4a0      1,048,600 
13     028c1c100218     028bde5b2910      2,097,240 Free
14     028c1c300270 7ffa9f2ac4a0      1,048,600 
15     028c1c400288     028bde5b2910      2,097,240 Free
16     028c1c6002e0 7ffa9f2ac4a0      1,048,600 
17 
18 Statistics:
19           MT Count  TotalSize Class Name
20 7ffa9f2ac4a0     7  7,340,200 System.Byte[]
21 028bde5b2910     7 12,583,472 Free
22 Total 14 objects, 19,923,672 bytes

我们可以看到,隔一个对象就是一个 Free 块,占据的内存还不小。我们选择一个 Free 块,查看一下它的前面和后面到底是什么东西。

复制代码
1 0:001> !gcroot 028c1bd00190 
2 HandleTable:
3     0000028bdfeb13e8 (strong handle)
4           -> 028be0400020     System.Object[] 
5           -> 028be2409f48     System.Collections.Generic.List<System.Byte[]> 
6           -> 028be2c021a8     System.Byte[][] 
7           -> 028c1bd00190     System.Byte[] 
8 
9 Found 1 unique roots.

我们在使用【!gcroot 028c1c000200】命令查看"028c1c000200"的跟引用。

复制代码
1 0:001> !gcroot 028c1c000200
2 HandleTable:
3     0000028bdfeb13e8 (strong handle)
4           -> 028be0400020     System.Object[] 
5           -> 028be2409f48     System.Collections.Generic.List\<System.Byte\[\]\> 
6           -> 028be2c021a8     System.Byte[][] 
7           -> 028c1c000200     System.Byte[] 
8 
9 Found 1 unique roots.

红色标注的,就是我们声明的类型 这个 List<byte[]?> list = new List<byte[]?>();我们可以使用【!do 028be2409f48】命令,查看 System.Collections.Generic.List<System.Byte[]> 是什么。

复制代码
 1 0:001> !do 028be2409f48 
 2 Name:        System.Collections.Generic.List`1[[System.Byte[], System.Private.CoreLib]]
 3 MethodTable: 00007ffa9f2ac620
 4 EEClass:     00007ffa9f291890
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.14\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa9f2ace40  400214c        8     System.__Canon[]  0 instance 0000028be2c021a8 _items(列表的数据项)11 00007ffa9f11e8d8  400214d       10         System.Int32  1 instance             1000 _size
12 00007ffa9f11e8d8  400214e       14         System.Int32  1 instance             1500 _version
13 00007ffa9f2ace40  400214f        8     System.__Canon[]  0   static dynamic statics NYI                 s_emptyArray

我们可以继续使用【!do 0000028be2c021a8】命令,查看他的数据详情。

复制代码
1 0:001> !DumpObj /d 0000028be2c021a8
2 Name:        System.Byte[][]
3 MethodTable: 00007ffa9f2acbe8
4 EEClass:     00007ffa9f2acb50
5 Tracked Type: false
6 Size:        8216(0x2018) bytes
7 Array:       Rank 1, Number of elements 1024, Type SZARRAY (Print Array)
8 Fields:
9 None

说明 List<> 底层是用一个二维数组实现的。当然,我们也可以使用【!da -details 0000028be2c021a8】命令查看数组详情。

复制代码
 1 0:001> !da -details 0000028be2c021a8
 2 Name:        System.Byte[][]
 3 MethodTable: 00007ffa9f2acbe8
 4 EEClass:     00007ffa9f2acb50
 5 Size:        8216(0x2018) bytes
 6 Array:       Rank 1, Number of elements 1024, Type SZARRAY
 7 Element Methodtable: 00007ffa9f2ac4a0
 8 [0] null
 9 [1] 0000028be3200078
10     Name:        System.Byte[]
11     MethodTable: 00007ffa9f2ac4a0
12     EEClass:     00007ffa9f2ac420
13     Tracked Type: false
14     Size:        1048600(0x100018) bytes
15     Array:       Rank 1, Number of elements 1048576, Type Byte     (Print Array)    
16     Content:         ....................................................................................................................    
17     Fields:
18     None
19 [2] null
20 [3] 0000028be33000b0
21     Name:        System.Byte[]
22     MethodTable: 00007ffa9f2ac4a0
23     EEClass:     00007ffa9f2ac420
24     Tracked Type: false
25     Size:        1048600(0x100018) bytes
26     Array:       Rank 1, Number of elements 1048576, Type Byte     (Print Array)    
27     Content:         ....................................................................................................................    
28     Fields:
29     None
30 [4] null
31 .....还有太多内容,省略了。

**          2.3、我们可以使用 Windbg 查找由于程序集泄露造成的内存泄漏。**               调试源码:Example_13_1_3
              当我们进入调试器界面,调试器是处于 int 3 中断状态的,我们需要使用【g】命令,继续运行程序,我们的程序会输出一系列数字,我是当数字到了657,我们点击【break】按钮暂停,开始调试我们的程序。

复制代码
 1 0:006> !eeheap -loader
 2 Loader Heap:
 3 --------------------------------------
 4 System Domain:     6fa9caf8
 5 LowFrequencyHeap:  00e20000(3000:3000) 062f0000(10000:2000) Size: 0x5000 (20480) bytes.
 6 HighFrequencyHeap: 00e24000(9000:1000) Size: 0x1000 (4096) bytes.
 7 StubHeap:          00e2d000(3000:1000) Size: 0x1000 (4096) bytes.
 8 Virtual Call Stub Heap:
 9   IndcellHeap:     00f60000(2000:1000) Size: 0x1000 (4096) bytes.
10   LookupHeap:      00f65000(2000:1000) Size: 0x1000 (4096) bytes.
11   ResolveHeap:     00f6b000(5000:1000) Size: 0x1000 (4096) bytes.
12   DispatchHeap:    00f67000(4000:1000) Size: 0x1000 (4096) bytes.
13   CacheEntryHeap:  00f62000(3000:1000) Size: 0x1000 (4096) bytes.
14 Total size:        Size: 0xc000 (49152) bytes.
15 --------------------------------------
16 Shared Domain:     6fa9c7a8
17 LowFrequencyHeap:  00e20000(3000:3000) 062f0000(10000:2000) Size: 0x5000 (20480) bytes.
18 HighFrequencyHeap: 00e24000(9000:1000) Size: 0x1000 (4096) bytes.
19 StubHeap:          00e2d000(3000:1000) Size: 0x1000 (4096) bytes.
20 Virtual Call Stub Heap:
21   IndcellHeap:     00f60000(2000:1000) Size: 0x1000 (4096) bytes.
22   LookupHeap:      00f65000(2000:1000) Size: 0x1000 (4096) bytes.
23   ResolveHeap:     00f6b000(5000:1000) Size: 0x1000 (4096) bytes.
24   DispatchHeap:    00f67000(4000:1000) Size: 0x1000 (4096) bytes.
25   CacheEntryHeap:  00f62000(3000:1000) Size: 0x1000 (4096) bytes.
26 Total size:        Size: 0xc000 (49152) bytes.
27 --------------------------------------
28 Domain 1:          00c1d890
29 LowFrequencyHeap:  00f40000(3000:3000) 00ff0000(10000:10000) 04f60000(10000:10000) 05080000(10000:10000) 05090000(10000:10000) 050a0000(10000:10000) 052d0000(10000:10000) 052e0000(10000:10000) 052f0000(10000:10000) 05300000(10000:10000) 05320000(10000:10000) 05340000(10000:10000) 05750000(10000:10000) 05770000(10000:10000) 05780000(10000:10000) 057a0000(10000:10000) 057b0000(10000:10000) 057d0000(10000:10000) 057e0000(10000:10000) 05800000(10000:10000) 05820000(10000:10000) 05830000(10000:10000) 05840000(10000:10000) 05850000(10000:10000) 05880000(10000:10000) 06090000(10000:10000) 060a0000(10000:10000) 060d0000(10000:10000) 060e0000(10000:10000) 060f0000(10000:10000) 06100000(10000:10000) 06120000(10000:10000) 06130000(10000:10000) 06150000(10000:10000) 06170000(10000:10000) 06180000(10000:10000) 06190000(10000:10000) 061b0000(10000:10000) 061d0000(10000:10000) 061e0000(10000:10000) 061f0000(10000:10000) 06220000(10000:10000) 06230000(10000:10000) 06240000(10000:10000) 06250000(10000:10000) 06280000(10000:10000) 06290000(10000:10000) 062a0000(10000:10000) 062c0000(10000:10000) 062d0000(10000:10000) 072d0000(10000:10000) 072e0000(10000:10000) 07300000(10000:10000) 07320000(10000:10000) 07330000(10000:10000) 07350000(10000:10000) 07360000(10000:10000) 07370000(10000:10000) 07380000(10000:10000) 073b0000(10000:10000) 073c0000(10000:10000) 073d0000(10000:10000) 07400000(10000:10000) 07410000(10000:10000) 07420000(10000:10000) 07430000(10000:10000) 07450000(10000:10000) 07470000(10000:10000) 07480000(10000:10000) 074a0000(10000:10000) 074b0000(10000:5000) Size: 0x458000 (4554752) bytes.
30 HighFrequencyHeap: 00f43000(a000:a000) 04f50000(10000:10000) 050b0000(10000:10000) 05310000(10000:10000) 05760000(10000:10000) 057c0000(10000:10000) 05810000(10000:10000) 05870000(10000:10000) 060b0000(10000:10000) 06110000(10000:10000) 06160000(10000:10000) 061c0000(10000:10000) 06210000(10000:10000) 06270000(10000:10000) 062b0000(10000:10000) 072f0000(10000:10000) 07340000(10000:10000) 073a0000(10000:10000) 073e0000(10000:10000) 07440000(10000:10000) 07490000(10000:7000) Size: 0x141000 (1314816) bytes.
31 StubHeap:          Size: 0x0 (0) bytes.
32 Virtual Call Stub Heap:
33   IndcellHeap:     00f50000(2000:1000) Size: 0x1000 (4096) bytes.
34   LookupHeap:      00f56000(1000:1000) Size: 0x1000 (4096) bytes.
35   ResolveHeap:     00f5a000(6000:2000) Size: 0x2000 (8192) bytes.
36   DispatchHeap:    00f57000(3000:1000) Size: 0x1000 (4096) bytes.
37   CacheEntryHeap:  00f52000(4000:1000) Size: 0x1000 (4096) bytes.
38 Total size:        Size: 0x59f000 (5894144) bytes.
39 --------------------------------------
40 Jit code heap:
41 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
42 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
43 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
44 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
45 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
46 LoaderCodeHeap:    00000000(0:0) Size: 0x0 (0) bytes.
47 ......
48 Module 0744c80c: Size: 0x0 (0) bytes.
49 Module 0744cfa4: Size: 0x0 (0) bytes.
50 Module 0744d73c: Size: 0x0 (0) bytes.
51 Module 0744ded4: Size: 0x0 (0) bytes.
52 Module 0744e66c: Size: 0x0 (0) bytes.
53 Module 0744ee04: Size: 0x0 (0) bytes.
54 Module 0744f59c: Size: 0x0 (0) bytes.
55 Module 07490010: Size: 0x0 (0) bytes.
56 Module 074907a4: Size: 0x0 (0) bytes.
57 Module 07490f3c: Size: 0x0 (0) bytes.
58 Module 074916d4: Size: 0x0 (0) bytes.
59 Module 07491e6c: Size: 0x0 (0) bytes.
60 Module 07492604: Size: 0x0 (0) bytes.
61 Module 07492d9c: Size: 0x0 (0) bytes.
62 Module 07493534: Size: 0x0 (0) bytes.
63 Module 07493ccc: Size: 0x0 (0) bytes.
64 Module 07494464: Size: 0x0 (0) bytes.
65 Module 07494bfc: Size: 0x0 (0) bytes.
66 Module 07495394: Size: 0x0 (0) bytes.
67 Module 07495b2c: Size: 0x0 (0) bytes.
68 Total size:      Size: 0x0 (0) bytes.
69 --------------------------------------
70 Total LoaderHeap size:   Size: 0x5b7000 (5992448) bytes.
71 =======================================

我们发现在【Loader Heap(加载堆)】里有很多 Module,我们随便选择一个 Module 查看,可以使用【!dumpModule】命令。

复制代码
 1 0:006> !DumpModule /d 07495b2c
 2 Name:       Unknown Module
 3 Attributes: Reflection 
 4 Assembly:   069a70c0
 5 LoaderHeap:              00000000
 6 TypeDefToMethodTableMap: 074b33d4
 7 TypeRefToMethodTableMap: 074b33e8
 8 MethodDefToDescMap:      074b33fc
 9 FieldDefToDescMap:       074b3424
10 MemberRefToDescMap:      00000000
11 FileReferencesMap:       074b3474
12 AssemblyReferencesMap:   074b3488

我们可以继续查看一下这个 Module 里面有多少 MT(方发表),有了 MT 就可以知道 MD(方法描述符),我们就能了解什么方法在起作用。

复制代码
 1 0:006> !dumpmt -md 07496260
 2 EEClass:         074b4958
 3 Module:          07495b2c
 4 Name:            <Unloaded Type>
 5 mdToken:         02000006
 6 File:            Unknown Module
 7 BaseSize:        0x14
 8 ComponentSize:   0x0
 9 Slots in VTable: 12
10 Number of IFaces in IFaceMap: 0
11 --------------------------------------
12 MethodDesc Table
13    Entry MethodDe    JIT Name
14 6e1e97b8 6ddec838 PreJIT System.Object.ToString()
15 6e1e96a0 6df28978 PreJIT System.Object.Equals(System.Object)
16 6e1f21f0 6df28998 PreJIT System.Object.GetHashCode()
17 6e1a4f2c 6df289a0 PreJIT System.Object.Finalize()
18 074c0e60 07496214    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.get_Reader()
19 0746f6ed 0749621c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.get_Writer()
20 0746f6f1 07496224   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.get_ReadMethods()
21 0746f6f5 0749622c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.get_WriteMethods()
22 0746f6f9 07496234   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.get_TypedSerializers()
23 0746f6fd 0749623c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.CanSerialize(System.Type)
24 0746f701 07496244   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract.GetSerializer(System.Type)
25 074c0e40 0749624c    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializerContract..ctor()

到这里,我们就看到了,这么多 Module 都有 GeneratedAssembly.XmlSerializerContract..ctor(),说明这就是问题所在。

2.4、我们可以使用 PerfView 查找由于程序集泄露造成的内存泄漏。
              调试源码:Example_13_1_3
              我们在 2.3 例子中使用 Windbg 查找了内存泄漏的原因,在这里,我们使用 PerfView 再来查找一次。
              我们首先将 Example_13_1_3 项目编译好,然后我们直接打开 PerfView 工具,效果如图:
              

我们点击【collect】菜单,选择【collect】子菜单,打开数据收集的窗口,重要操作我已经使用红色标记了。

Additional Providers的值,然后加上记录栈的key,即 @StacksEnabled=true,合并后就是::Microsoft-Windows-DotNETRuntime:LoaderKeyword:Always:@StacksEnabled=true

复制代码
1 Microsoft-Windows-DotNETRuntime:LoaderKeyword:Always:@StacksEnabled=true

这个值是需要配置的,我们可以点开【Provider Browser】按钮进行配置。效果如图:

配置好以后,我们就可以点击【Start Collection】开始收集数据了,效果如图:

Prefview 开始收集数据以后,我们打开我们的项目程序,也就是 exe 程序,直接运行。我等程序运行到2000左右就关闭程序,同时也点击【Stop Collection】停止 Perfview 的收集。我们需要等待一下,Perfview 执行完毕,生成zip 文件,效果如图:

我们双击【Events】打开了事件窗口,在弹框中搜索 AssemblyLoad 事件,然后在【Time MSec】 列点击右键选择 【Open Any Stacks】 打开此次加载的 线程调用栈, 如下图所示:
            

右键【Open Any Stacks】打开一个【Any Stacks】新窗口,我们就可以查看详情了,效果如图:

好了,这个过程终于完了,挺困难的,这个过程我搞了好多遍才有说收获。

四、总结
    终于写完了。还是老话,虽然很忙,写作过程也挺累的,但是看到了自己的成长,心里还是挺快乐的。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。

相关推荐
蓑衣夜行3 天前
捉虫笔记(五)-WinDbg调试3个时机
windbg·捉虫笔记
zhuqiyua3 天前
深入解析Kernel32.dll与Msvcrt.dll
汇编·microsoft·windbg·二进制·dll
可均可可2 个月前
Advanced .Net Debugging 11:完结篇
windbg·sos·net 高级调试·ntsd
鬼才2 个月前
记一次 公司.NET项目部署在Linux环境压测时 内存暴涨分析
windbg·dump
可均可可5 个月前
Advanced .Net Debugging 10:事后调试
windbg·任务管理器·dump·ntsd·事后调试