一、介绍
今天是《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】新窗口,我们就可以查看详情了,效果如图:
好了,这个过程终于完了,挺困难的,这个过程我搞了好多遍才有说收获。
四、总结
终于写完了。还是老话,虽然很忙,写作过程也挺累的,但是看到了自己的成长,心里还是挺快乐的。学习过程真的没那么轻松,还好是自己比较喜欢这一行,否则真不知道自己能不能坚持下来。老话重谈,《高级调试》的这本书第一遍看,真的很晕,第二遍稍微好点,不学不知道,一学吓一跳,自己欠缺的很多。好了,不说了,不忘初心,继续努力,希望老天不要辜负努力的人。