引言 本片文章分享一下之前遇到的WPF應用在觸摸屏下使用時的兩個問題。 場景 具體場景就是一個配置界面, ScrollViewer 中包含一個StackPanel 然後縱向堆疊,已滾動的方式查看,然後包含多個 TextBlock 、 TextBox 以及DataGrid ,期間遇到了兩個問題: WP ...
一、介紹
這是我的《Advanced .Net Debugging》這個系列的第四篇文章。今天這篇文章的標題雖然叫做“基本調試任務”,但是這章的內容還是挺多的。由於內容太多,故原書的第三章內容我分兩篇文章來寫。上一篇我們瞭解了一些調試技巧,比如:單步調試、下斷點、過程調試等,這篇文章主要涉及的內容是對象的轉儲,記憶體的轉儲,值類型的轉儲,引用類型的轉儲、數組的轉儲、異常的轉儲等。第一次說到“轉儲”,可能大家不知道什麼意思,其實就是把我們想要的內容輸出出來或者說是列印出來,方便我們分析問題。SOSEX擴展的內容我就省略了,因為我這個系列的是基於 .NET 8 版本來寫的,SOSEX是基於 .NET Framework 版本的,如果大家想瞭解其內容,可以查看我的【高級調試】系列(我當前寫的是《Advanced .Net Debugging》系列,是不一樣的),當然,也可以看原書。【高級調試】系列主要是集中在 .NET Framework 版本的。如果我們想成為一名合格程式員,這些調試技巧都是必須要掌握的。
如果在沒有說明的情況下,所有代碼的測試環境都是 Net 8.0,如果有變動,我會在項目章節里進行說明。好了,廢話不多說,開始我們今天的調試工作。
調試環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
操作系統:Windows Professional 10
調試工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
下載地址:可以去Microsoft Store 去下載
開發工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源碼:源碼下載
二、調試源碼
廢話不多說,本節是調試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。
2.1、ExampleCore_3_1_6
1 using System.Diagnostics; 2 3 namespace ExampleCore_3_1_6 4 { 5 public class ObjTypes 6 { 7 public struct Coordinate 8 { 9 public int xCord; 10 public int yCord; 11 public int zCord; 12 13 public Coordinate(int x, int y, int z) 14 { 15 xCord = x; 16 yCord = y; 17 zCord = z; 18 } 19 } 20 21 private Coordinate coordinate; 22 23 int[] intArray = [1, 2, 3, 4, 5]; 24 25 string[] strArray = ["Welcome", "to", "Advanced", ".NET", "Debugging"]; 26 27 static void Main(string[] args) 28 { 29 Coordinate point = new Coordinate(100, 100, 100); 30 Console.WriteLine("Press any key to continue(AddCoordinate)"); 31 Console.ReadKey(); 32 ObjTypes ob = new ObjTypes(); 33 ob.AddCoordinate(point); 34 35 Console.WriteLine("Press any key to continue(Arrays)"); 36 Console.ReadKey(); 37 ob.PrintArrays(); 38 39 Console.WriteLine("Press any key to continue(Generics)"); 40 Console.ReadKey(); 41 Comparer<int> c = new Comparer<int>(); 42 Console.WriteLine("Greater:{0}", c.GreaterThan(5, 10)); 43 44 Console.WriteLine("Preaa any key to continue(Exception)"); 45 Console.ReadKey(); 46 ob.ThrowException(null); 47 } 48 49 public void AddCoordinate(Coordinate coord) 50 { 51 coordinate.xCord += coord.xCord; 52 coordinate.yCord += coord.yCord; 53 coordinate.zCord += coord.zCord; 54 55 Console.WriteLine("x:{0},y:{1},z:{2}", coordinate.xCord, coordinate.yCord, coordinate.zCord); 56 } 57 58 public void PrintArrays() 59 { 60 foreach (int i in intArray) 61 { 62 Console.WriteLine("Int:{0}", i); 63 } 64 foreach (string s in strArray) 65 { 66 Console.WriteLine("Str:{0}", s); 67 } 68 } 69 70 public void ThrowException(ObjTypes? obj) 71 { 72 if (obj == null) 73 { 74 throw new ArgumentException("Obj cannot be null"); 75 } 76 } 77 } 78 public class Comparer<T> where T : IComparable 79 { 80 public T GreaterThan(T d, T d2) 81 { 82 int ret = d.CompareTo(d2); 83 if (ret > 0) 84 { 85 return d; 86 } 87 else 88 { 89 return d2; 90 } 91 } 92 93 public T LessThan(T d, T d2) 94 { 95 int ret = d.CompareTo(d2); 96 if (ret < 0) 97 { 98 return d; 99 } 100 else 101 { 102 return d2; 103 } 104 } 105 } 106 }View Code
2.2、ExampleCore_3_1_7
1 namespace ExampleCore_3_1_7 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 var person = new Person(); 8 Console.ReadLine(); 9 } 10 } 11 internal class Person 12 { 13 public int Age = 20; 14 15 public string Name = "jack"; 16 } 17 }View Code
2.3、ExampleCore_3_1_8
1 namespace ExampleCore_3_1_8 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Console.WriteLine("Welcome to .NET Advanced Debugging!"); 8 9 Person person = new Person() { Name = "PatrickLiu", Age = 32, HomeAddress = new Address() { Country = "China", Province = "冀州", City = "直隸總督府", Region = "廣平大街23號", PostalCode = "213339" } }; 10 11 Console.WriteLine($"名稱:{person.Name},地址:{person.HomeAddress}"); 12 13 Console.Read(); 14 } 15 } 16 17 public class Person 18 { 19 public int Age { get; set; } 20 public string? Name { get; set; } 21 public Address? HomeAddress { get; set; } 22 } 23 24 public class Address 25 { 26 public string? Country { get; set; } 27 public string? Province { get; set; } 28 public string? City { get; set; } 29 public string? Region { get; set; } 30 public string? PostalCode { get; set; } 31 public override string ToString() 32 { 33 return $"{Country}-{Province}-{City}-{Region}-{PostalCode}"; 34 } 35 } 36 }View Code
三、基礎知識
本節的內容也很多,本來打算這篇文章分為:3.1、3.2、3.3、3.4、3.5、3.6 共 6 節就將原書的第三章剩下的內容全部寫完,但是內容太多,就只保留一節了。便於學習和閱讀。下一篇,怎麼排版再定吧。
3.1、對象檢查
本節,我們將介紹一些命令用來分析程式的狀態,以確定程式的故障。我們先來介紹非托管調試器中一些常用的命令,然後在介紹在 SOS 調試擴展中針對托管代碼調試的命令。
3.1.1、記憶體轉儲
A、基礎知識
在調試器中有很多命令都可以轉儲記憶體的內容,這個方式非常底層,從記憶體地址上觀察地址上的內容。最常使用的命令是【d(顯示記憶體)】,比如:【dp】命令。根據轉儲的數據類型不同,命令【d】也有很多不同的變化,比如:du,dw,db,da 等,如果想瞭解更多,可以查看 Windbg 的幫助文檔,命令是【.hh】。
其他一些變化形式:
。du 命令把被轉儲的記憶體視作為 Unicode 字元。
。da 命令把被轉儲的記憶體視作為 ASCII字元。
。dw 命令把被轉儲的記憶體視作為字(word)。
。db 命令把被轉儲的記憶體視作為位元組值和 ASCII 字元。
。dq 命令把被轉儲的記憶體視作為四字值(quad word)。
B、眼見為實:
1)、NTSD 調試
調試源碼:ExampleCore_3_1_7
我們編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.2】,輸入命令:NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.EXE。
打開【NTSD】調試器視窗。
繼續使用【g】命令,運行調試器,等調試器卡住後,按【ctrl+c】組合鍵鍵入調試器的中斷模式。
切換到主線程,執行命令【~0s】。
1 0:009> ~0s 2 coreclr!GetThreadNULLOk+0x1e [inlined in coreclr!CrstBase::Enter+0x32]: 3 00007ff9`bd119fa2 488b34c8 mov rsi,qword ptr [rax+rcx*8] ds:0000026c`600cb0b0=0000026c600ba2d0
使用【!clrstack -a】查看托管代碼的線程調用棧。
1 0:000> !clrstack -a 2 OS Thread Id: 0x954 (0) 3 Child SP IP Call Site 4 000000785C77DCE8 00007ff9bd119fa2 [ExternalMethodFrame: 000000785c77dce8] 5 ......(省略了) 6 000000785C77E4D0 00007FF95D621987 ExampleCore_3_1_7.Program.Main(System.String[]) 7 PARAMETERS: 8 args (0x000000785C77E520) = 0x0000026c64808ea0 9 LOCALS: 10 0x000000785C77E508 = 0x0000026c64809640
0x0000026c64809640 紅色標註的就是 Person 類型的局部變數 person 的地址。我們可以使用【!dumpobj /d 0x0000026c64809640】查看 person 的詳情。
1 0:000> !dumpobj /d 0x0000026c64809640 2 Name: ExampleCore_3_1_7.Person 3 MethodTable: 00007ff95d6d93e0(方法表地址) 4 EEClass: 00007ff95d6e1f18 5 Tracked Type: false 6 Size: 32(0x20) bytes 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff95d591188 4000001 10 System.Int32 1 instance 20 Age 11 00007ff95d60ec08 4000002 8 System.String 0 instance 000002acf67a04a0 Name 12 0:000>
同樣,我們可以使用【dp】命令也能看到 person 的詳情信息,只是不是很直觀。
1 0:000> dp 0x0000026c64809640 2 0000026c`64809640 00007ff9`5d6d93e0 000002ac`f67a04a0 3 0000026c`64809650 00000000`00000014 00000000`00000000 4 0000026c`64809660 00007ff9`5d555fa8 00000000`00000000 5 0000026c`64809670 00000000`00000000 00007ff9`5d60ec08 6 0000026c`64809680 0065006b`0000000c 006c0065`006e0072 7 0000026c`64809690 0064002e`00320033 00000000`006c006c 8 0000026c`648096a0 00000000`00000000 00007ff9`5d700d68 9 0000026c`648096b0 00000000`00000000 00000000`00000000
上面使用【!dumpobj】和【dp】命令我們找到的方法表地址都是一樣的。000002ac`f67a04a0 這個項就是 Name 域的地址,因為 Name 是引用類型,所以這裡是一個地址。我們可以繼續使用【!dumpobj /d 000002ac`f67a04a0】來驗證。
1 0:000> !dumpobj /d 000002ac`f67a04a0 2 Name: System.String 3 MethodTable: 00007ff95d60ec08 4 EEClass: 00007ff95d5ea500 5 Tracked Type: false 6 Size: 30(0x1e) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: jack(我們賦的值) 9 Fields: 10 MT Field Offset Type VT Attr Value Name 11 00007ff95d591188 400033b 8 System.Int32 1 instance 4 _stringLength 12 00007ff95d59b538 400033c c System.Char 1 instance 6a _firstChar 13 00007ff95d60ec08 400033a c8 System.String 0 static 000002acf67a0008 Empty
我們可以使用【dumpobj】命令,當然也可以使用【dp】命令,都可以看到想看到的信息,不過是【dp】可讀性差很多。
如果我們查看命令如何使用,可以使用【.hh】命令。
1 0:000> .hh
效果如圖:
就能打開調試的幫助文件。
我們可以打開【索引】選項,查看我們想要查看的命令。
2)、Windbg Preview 調試
調試源碼:ExampleCore_3_1_7
我們編譯項目,打開 Windbg,點擊【文件】----》【Launch executable】附加程式 ExampleCore_3_1_7.exe,打開調試器的界面,程式已經處於中斷狀態。由於顯示的內容太多,我們可以使用【.cls】命令清空調試器的界面。我們使用【g】命令繼續運行程式,調試器會在【Console.ReadLine()】這行代碼處卡住,我們點擊【break】按鈕,就可以調試程式了。
查看是否在主線程,如果不是,切換到主線程,執行命令【~0s】。
1 0:006> ~0s 2 ntdll!NtReadFile+0x14: 3 00007ffa`9576ae54 c3 ret
我們查看一下當前托管的線程棧,執行命令【!clrstack -a】。
1 0:000> !clrstack -a 2 OS Thread Id: 0x8fc (0) 3 Child SP IP Call Site 4 000000D30A57E230 00007ffa9576ae54 [InlinedCallFrame: 000000d30a57e230] 5 000000D30A57E230 00007ff9e4f076eb [InlinedCallFrame: 000000d30a57e230] 6 ......(省略了) 7 8 000000D30A57E580 00007ff95e471987 ExampleCore_3_1_7.Program.Main(System.String[]) [E:\Visual Studio\..\ExampleCore_3_1_7\Program.cs @ 8] 9 PARAMETERS: 10 args (0x000000D30A57E5D0) = 0x0000021dc3808ea0 11 LOCALS: 12 0x000000D30A57E5B8 = 0x0000021dc3809640
0x0000021dc3809640 紅色標註的就是 Person 類型的局部變數 person 的地址。我們直接使用【dp】命令轉儲出 person 的值。
1 0:000> dp 0x0000021dc3809640 2 0000021d`c3809640 00007ff9`5e5293e0 0000025e`558d04a0 3 0000021d`c3809650 00000000`00000014 00000000`00000000 4 0000021d`c3809660 00007ff9`5e3a5fa8 00000000`00000000 5 0000021d`c3809670 00000000`00000000 00007ff9`5e45ec08 6 0000021d`c3809680 0065006b`0000000c 006c0065`006e0072 7 0000021d`c3809690 0064002e`00320033 00000000`006c006c 8 0000021d`c38096a0 00000000`00000000 00007ff9`5e550d68 9 0000021d`c38096b0 00000000`00000000 00000000`00000000
最左邊的一列給出了每行記憶體的起始地址,後面是記憶體的內容。00007ff9`5e5293e0 紅色標註的就是方法表。我們可以驗證。執行命令【!dumpheap -type Person】。
1 0:000> !dumpheap -type Person 2 Address MT Size 3 021dc3809640 7ff95e5293e0 32 4 5 Statistics: 6 MT Count TotalSize Class Name 7 7ff95e5293e0 1 32 ExampleCore_3_1_7.Person 8 Total 1 objects, 32 bytes
7ff95e5293e0 和 00007ff9`5e5293e0 兩個值是一樣的。
雖然直接輸出記憶體的內容很有用,但是閱讀起來就很麻煩。當我們調試托管代碼的時候,使用 SOS 擴展命令會提供更直接的信息。
如果我們想查看有關調試器的各種命令。我們可以使用【.hh】幫助文件。
1 windbg> .hh
同樣能打開調試器的幫助視窗。
3.1.2、值類型的轉儲
A、基礎知識
我們知道.NET 的類型分為值類型和引用類型,那我們如何判斷一個指針指向的是否是值類型呢,最佳的方式就是使用【dumpobj】命令,但它只對引用類型有效。【dumpobj】命令的參數是一個指向引用類型的指針,如果指針指向的是值類型,【dumpobj】命令就會輸出:<Note:this object has an invalid CLASS field>Invalid object。
B、眼見為實
1)、NTSD 調試
調試源碼:ExampleCore_3_1_6
1.1)、查看獨立的值類型
編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.exe】。
打開【NTSD】調試器視窗。
輸出內容太多,使用【.cls】命令,清理一下屏幕。然後使用【g】命令,運行調試器。調試器輸出:Press any key to continue(AddCoordinate),如圖:
輸入【ctrl+c】組合鍵進入中斷模式。我們現在查看一下托管線程棧,可以使用【!clrstack -a】命令。
1 0:002> !clrstack -a 2 OS Thread Id: 0x1764 (2) 3 Unable to walk the managed stack. The current thread is likely not a 4 managed thread. You can run !threads to get a list of managed threads in 5 the process 6 Failed to start stack walk: 80070057
如圖:
該命令執行錯誤,提示不是一個有效的托管線程,由於我們是手動中斷程式的執行,調試器的線程上下文是在調試器線程上,它是一個非托管線程,因此,在執行該命令之前,需要切換到托管線程的上下文中。執行命令【~0s】。
1 0:002> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffb`2fdaae74 c3 ret 4 0:000>
我們再次執行【!clrstack -a】命令。
1 0:000> !clrstack -a 2 OS Thread Id: 0x2c1c (0) 3 Child SP IP Call Site 4 000000CD41F7E5E8 00007ffb2fdaae74 [InlinedCallFrame: 000000cd41f7e5e8] 5 000000CD41F7E5E8 00007ffb1b68787a [InlinedCallFrame: 000000cd41f7e5e8] 6 。。。。。。(省略了) 7 000000CD41F7E770 00007FF9F55919B6 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) 8 PARAMETERS: 9 args (0x000000CD41F7E840) = 0x000002a492808ea0 10 LOCALS: 11 0x000000CD41F7E820 = 0x0000006400000064 12 0x000000CD41F7E818 = 0x0000000000000000 13 0x000000CD41F7E810 = 0x0000000000000000
這時,【clrstack】命令輸出了托管線程的棧回溯,包括每個棧幀的局部變數和參數。我們主要關註【ExampleCore_3_1_6.ObjTypes.Main】棧幀和地址【0x000000CD41F7E820】上的局部變數。【0x000000CD41F7E820】這個地址我們不知道它指向的是一個值類型還是引用類型。我們可以使用【dumpobj】命令做一個測試,因為該命令只對引用類型實例起作用。
1 0:000> !dumpobj