Net 高階偵錯之十三:託管堆的幾個經典破壞問題

2023-12-13 12:00:23
一、介紹
      今天是《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】新視窗,我們就可以檢視詳情了,效果如圖:
            

            好了,這個過程終於完了,挺困難的,這個過程我搞了好多遍才有說收穫。


四、總結
    終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裡還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高階偵錯》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。