今天是《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 }
第二部分,我還使用 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 //}
在這個項目裏還有一個 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 }
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 }
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.
我們可以使用【!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】新窗口,我們就可以查看詳情了,效果如圖:
好了,這個過程終於完了,挺困難的,這個過程我搞了好多遍纔有說收穫。
四、總結
終於寫完了。還是老話,雖然很忙,寫作過程也挺累的,但是看到了自己的成長,心裏還是挺快樂的。學習過程真的沒那麼輕鬆,還好是自己比較喜歡這一行,否則真不知道自己能不能堅持下來。老話重談,《高級調試》的這本書第一遍看,真的很暈,第二遍稍微好點,不學不知道,一學嚇一跳,自己欠缺的很多。好了,不說了,不忘初心,繼續努力,希望老天不要辜負努力的人。