这是我的《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、内存转储
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.2、值类型的转储
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.3、转储基本的引用类型
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.4、数组的转储
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.5、栈上对象的转储
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.6、找出对象的大小
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
2.7、异常的转储
A、基础知识
B、眼见为实
1)、使用【NTSD】调试
2)、使用【Windbg Preview】调试
三、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
3.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 }
3.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 }
3.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 }
四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。
4.1、对象检查
在大多数调试会话中,首先需要执行的任务之一就是分析应用程序的状态,以便确认应用程序的故障是由于某种无效状态造成的。本节,我们将介绍一些命令用来分析程序的状态,以确定程序的故障。我们先来介绍非托管调试器中一些常用的命令,然后再介绍在 SOS 调试扩展中针对托管代码调试的命令。
4.1.1、内存转储
A、基础知识
在调试器中有很多命令都可以转储内存的内容,这个方式非常底层,从内存地址上观察地址上的内容。最常使用的命令是【d(显示内存)】,比如:【dp】命令。根据转储的数据类型不同,命令【d】也有很多不同的变化,比如:du,dw,db,da 等,如果想了解更多,可以查看 Windbg 的帮助文档,命令是【.hh】。
其他一些变化形式:
。du 命令把被转储的内存视作为 Unicode 字符。
。da 命令把被转储的内存视作为 ASCII字符。
。dw 命令把被转储的内存视作为字(word)。
。db 命令把被转储的内存视作为字节值和 ASCII 字符。
。dq 命令把被转储的内存视作为四字值(quad word)。
.NET 类型共分为两种:值类型和引用类型 。值类型是从 System.ValueType 类型继承下来的,一般在线程栈上分配存储空间(指方法的参数和局部变量)。如果值类型包在了引用类型的内部,它会和引用类型一起在托管堆上存储。引用类型是从 System.Object 类型继承下来的,并且是在托管堆上分配存储空间的。
我们知道所有类型的基类是 System.Object ,包括值类型。前面已经介绍过,值类型被分配在栈上,而引用类型则被分配在托管堆上。因此,如何把一个在栈上分配的值赋给一个在托管堆上分配的对象?答案是装箱操作。当把值类型赋给引用类型时,CLR将自动执行以下任务:
1)在托管堆上分配内存。
2)将值类型的内存(从栈上)复制到托管堆上新分配的引用类型中。
与装箱相反的操作是拆箱,这个操作能将引用类型赋值给值类型。严格来说,拆箱操作的开销小于装箱操作的开销。因为当执行拆箱操作时,CLR并不会复制被装箱的值类型的内容,而是返回一个托管指针,指向包含在引用类型中的值类型实例。然而,在赋值操作中同样也包含拆箱操作,从而需要将一个被装箱的值类型复制到栈上,这个操作也会带来一定的开销。
虽然这些操作对用户来说是透明的,但了解这些操作的内部机制是很重要的,因为它们能对调试会话产生影响(并且,当存在大量的装箱和拆箱等操作时,会对性能产生影响)。
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
同样能打开调试器的帮助窗口。
4.1.2、值类型的转储
A、基础知识
我们知道.NET 的类型分为值类型和引用类型,那我们如何判断一个指针指向的是否是值类型呢,最佳的方式就是使用【dumpobj】命令,但它只对引用类型有效。【dumpobj】命令的参数是一个指向引用类型的指针,如果指针指向的是值类型,【dumpobj】命令就会输出:<Note:this object has an invalid CLASS field>Invalid object。
在前面已经提到过,如果一个值类型没有作为某个引用类型的一部分(例如,在某个函数中声明一个局部变量或者一个值类型),那么它会被分配在栈上,并且可以使用前面介绍的d系列命令来转储值类型的内容。
我们在使用调试器调试程序的时候,需要注意,如果我们是手动中断调试器的执行,执行有些命令会报错,比如:【!clrstack 】命令。当【!ClrStack】命令提示了一个错误,指出当前的线程上下文并不是一个有效的托管线程。由于我们手动地中断程序的执行,调试器的线程上下文是在调试器线程上,而这是一个非托管线程,因此在运行【!ClrStack】命令之前,必须首先切换到托管线程上下文。使用~命令将上下文切换到线程0,然后再次执行ClrStack命令。
如果我们想转储值类型的内容,可以使用【!DumpVC <Method Table> <Value object start addr>】命令,<Method Table>:表示是方法表的地址,<Value object start addr>:表示的就是值类型的地址。
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 0x000000CD41F7E820 2 <Note: this object has an invalid CLASS field> 3 Invalid object
从这个输出结果可以知道,这个指针指向的肯定不是引用类型。我们可以通过命令【r】验证一下,可以观察【rsp】寄存器,它保存的是当前栈指针。
1 0:000> r 2 rax=0000000000000007 rbx=000000cd41f7e550 rcx=00000000000001c8 3 rdx=0000000000000000 rsi=0000000000000000 rdi=00000000000001c8 4 rip=00007ffb2fdaae74 rsp=000000cd41f7e388 rbp=000000cd41f7e490 5 r8=000000cd41f7e388 r9=000000cd41f7e490 r10=0000000000000000 6 r11=0000000000000246 r12=000000cd41f7e8a0 r13=0000000000000004 7 r14=0000000000000003 r15=000000cd41f7ea00 8 iopl=0 nv up ei pl zr na po nc 9 cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000244 10 ntdll!NtDeviceIoControlFile+0x14: 11 00007ffb`2fdaae74 c3 ret
【rsp】寄存器的值是:000000CD41F7E388,我们正在分析的地址是:0x000000CD41F7E820,连个地址非常接近,说明我们分析的地址是栈地址,也就是这个地址存储的是值类型的值。
我们可以使用【dp】命令查看一下具体的内容。
1 0:000> dp 0x000000CD41F7E820 2 000000cd`41f7e820 00000064`00000064 00000000`00000064 3 000000cd`41f7e830 000000cd`41f7e860 00007ffa`550da1a3 4 000000cd`41f7e840 000002a4`92808ea0 000000cd`41f7ee88 5 000000cd`41f7e850 000000cd`41f7ee88 000000cd`41f7ea79 6 000000cd`41f7e860 000000cd`41f7e910 00000000`0000001d 7 000000cd`41f7e870 000000cd`41f7ea88 00007ffa`550614c9 8 000000cd`41f7e880 00000000`00000000 00000000`00000130 9 000000cd`41f7e890 000000cd`41f7ea88 00007ffa`54f8a456
00000064 的十进制的值就是100。后面有三个域的值是0x64。
1 0:000> ? 00000064 2 Evaluate expression: 100 = 00000000`00000064
1.2)、查看嵌入在引用类型中的值类型。
编译项目,打开【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),如图:
1 0:009> !bpmd ExampleCore_3_1_6 ExampleCore_3_1_6.ObjTypes.AddCoordinate 2 MethodDesc = 00007FF9F5630108 3 Adding pending breakpoints...
我们继续运行调试器。
0:000> g (131c.3ca0): CLR notification exception - code e0444143 (first chance) JITTED ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate) Setting breakpoint: bp 00007FF9F43B1CD0 [ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)] 。。。
重新设置断点,除非并中断执行。我们执行【!clrstack -a】命令查看一下托管线程栈。
1 0:000> !clrstack -a 2 OS Thread Id: 0x3df8 (0) 3 Child SP IP Call Site 4 00000085A277E468 00007ffb2d7f9202 [HelperMethodFrame: 00000085a277e468] System.Diagnostics.Debugger.BreakInternal() 5 00000085A277E570 00007FFA4FC654DA System.Diagnostics.Debugger.Break() 6 7 00000085A277E5A0 00007FF9F0AB1D1A ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate) 8 PARAMETERS: 9 this (0x00000085A277E600) = 0x00000264de809ce0 10 coord (0x00000085A277E628) = 0x0000006400000064 11 12 00000085A277E600 00007FF9F0AB1A11 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) 13 PARAMETERS: 14 args (0x00000085A277E6D0) = 0x00000264de808ea0 15 LOCALS: 16 0x00000085A277E6B0 = 0x0000006400000064 17 0x00000085A277E6A8 = 0x00000264de809ce0 18 0x00000085A277E6A0 = 0x0000000000000000
我们主要关注【ExampleCore_3_1_6.ObjTypes.AddCoordinate】栈帧和【this】指针。【this】指针指向的是当前的引用类型实例。我们可以使用【dumpobj】命令输出看一下。
1 0:000> !dumpobj /d 0x00000264de809ce0 2 Name: ExampleCore_3_1_6.ObjTypes 3 MethodTable: 00007ff9f0b60238 4 EEClass: 00007ff9f0b4fcb8 5 Tracked Type: false 6 Size: 48(0x30) bytes 7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff9f0b601c8 4000001 18 ...jTypes+Coordinate 1 instance 00000264de809cf8 coordinate 11 00007ff9f0a99df8 4000002 8 System.Int32[] 0 instance 00000264de809d10 intArray 12 00007ff9f0b3fac8 4000003 10 System.String[] 0 instance 00000264de809d80 strArray
红色标注的【coordinate】就是引用类型包含一个值类型的例子。【MT、Field、Offset、Type、VT、Attr、Value和 Name】这些域具体的意思,可以查看图表。
我们有两种方式可以查看【coordinate】变量的具体值。第一种,使用【dp】命令。命令的参数分别是引用类型对象的地址(0x00000264de809ce0)和偏移(0x18)。
1 0:000> dp 0x00000264de809ce0+0x18 2 00000264`de809cf8 00000000`00000000 00000000`00000000 3 00000264`de809d08 00000000`00000000 00007ff9`f0a99df8 4 00000264`de809d18 00000000`00000005 00000002`00000001 5 00000264`de809d28 00000004`00000003 00000000`00000005 6 00000264`de809d38 00000000`00000000 00007ff9`f0bb4468 7 00000264`de809d48 00000000`00000000 00000000`00000000 8 00000264`de809d58 00000000`00000000 00000000`00000000 9 00000264`de809d68 00000000`00000000 00007ff9`f0bb3de0
第二种就是使用【!dumpvc mt addr】直接查看值类型,更直接。
1 0:000> !dumpvc 00007ff9f0b601c8 00000264de809cf8 2 Name: ExampleCore_3_1_6.ObjTypes+Coordinate 3 MethodTable: 00007ff9f0b601c8 4 EEClass: 00007ff9f0b4fd30 5 Size: 32(0x20) bytes 6 File: E:\Visual Studio 2022\Source\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 7 Fields: 8 MT Field Offset Type VT Attr Value Name 9 00007ff9f0a21188 4000005 0 System.Int32 1 instance 0 xCord 10 00007ff9f0a21188 4000006 4 System.Int32 1 instance 0 yCord 11 00007ff9f0a21188 4000007 8 System.Int32 1 instance 0 zCord
2)、使用 【Windbg Preview】 调试
调试源码:ExampleCore_3_1_6
2.1)、查看独立的值类型
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,控制台程序输出:Press any key to continue(AddCoordinate),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
首先,我们希望显示出托管调用栈以及相关的局部变量。使用【!clrstack -a】命令获取这些信息。
1 0:001> !clrstack -a 2 OS Thread Id: 0x239c (1) 3 Unable to walk the managed stack. The current thread is likely not a 4 managed thread. You can run !clrthreads to get a list of managed threads in 5 the process 6 Failed to start stack walk: 80070057
【clrstack】命令出错了,错误原因是当前的线程上下文并不是一个有效的托管线程。由于我们手动的中断程序的执行,调试器的线程上下文是在调试器线程上,而这个线程是非托管的线程。因此,在执行【clrstack】命令之前,首先必须切换到托管线程上下文。使用【~】命令将上下文切换到线程0。
1 0:001> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`9576ae74 c3 ret
然后,再次执行【!clrstack -a】命令。
1 0:000> !clrstack -a 2 OS Thread Id: 0x584 (0) 3 Child SP IP Call Site 4 000000F06737E948 00007ffa9576ae74 [InlinedCallFrame: 000000f06737e948] 5 000000F06737E948 00007ffa4f72787a [InlinedCallFrame: 000000f06737e948] 6 ......(省略了) 7 8 000000F06737EAD0 00007ff95dd019b6 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 29] 9 PARAMETERS: 10 args (0x000000F06737EBA0) = 0x0000020f54808ea0 11 LOCALS: 12 0x000000F06737EB80 = 0x0000006400000064 13 0x000000F06737EB78 = 0x0000000000000000 14 0x000000F06737EB70 = 0x0000000000000000
这就是【!clrstack】命令输出的托管线程的栈回溯,包括每个栈帧的局部变量和参数。在调用栈中,我们主要关注的是 ExampleCore_3_1_6.ObjTypes.Main 栈帧和位于地址 0x000000F06737EB80 上的局部变量(红色标注的)。
由于我们不知道这个局部变量指向的是值类型还是引用类型,因此我们可以使用【!dumpobj】做一下判断。
1 0:000> !dumpobj 0x000000F06737EB80 2 <Note: this object has an invalid CLASS field> 3 Invalid object
从命令的输出结果中可以看出,该地址的肯定不是引用类型。我们来验证,值类型被保存在栈上,如果发现某个地址位于当前栈指针的附近,那就可以证明是值类型了。我们使用【r】命令,观察 rsp 寄存器(保存的是当前栈指针)。
1 0:000> r 2 rax=0000000000000007 rbx=000000f06737e8b0 rcx=0000000000000058 3 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000058 4 rip=00007ffa9576ae74 rsp=000000f06737e6e8 rbp=000000f06737e7f0 5 r8=000000f06737e6e8 r9=000000f06737e7f0 r10=0000000000000000 6 r11=0000000000000130 r12=000000f06737ec00 r13=0000000000000004 7 r14=0000000000000003 r15=000000f06737ed60 8 iopl=0 nv up ei pl zr na po nc 9 cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000244 10 ntdll!NtDeviceIoControlFile+0x14: 11 00007ffa`9576ae74 c3 ret
在 rsp 寄存器中包含的值:000000f06737e6e8,它非常接近我们分析的地址:0x000000F06737EB80。说明我们分析的地址就是一个栈地址,我们使用【dp】查看一下详情。
1 0:000> dp 0x000000F06737EB80 2 000000f0`6737eb80 00000064`00000064 00000000`00000064 3 000000f0`6737eb90 000000f0`6737ebc0 00007ff9`bd85a1a3 4 000000f0`6737eba0 0000020f`54808ea0 000000f0`6737f1e8 5 000000f0`6737ebb0 000000f0`6737f1e8 000000f0`6737edd9 6 000000f0`6737ebc0 000000f0`6737ec70 00000000`0000001d 7 000000f0`6737ebd0 000000f0`6737ede8 00007ff9`bd7e14c9 8 000000f0`6737ebe0 00000000`00000000 00000000`00000130 9 000000f0`6737ebf0 000000f0`6737ede8 00007ff9`bd70a456
00000064 红色标注的十进制的值就是100。
1 0:000> ? 00000064 2 Evaluate expression: 100 = 00000000`00000064
有三个 00000064,分别对应 Coordinate 类型的各个域。
2.2)、查看引用类型中的值类型【2.1 的例子】我们看到了如何显示在函数内声明的值类型的内容。通常,值类型被嵌入在引用类型中并被保存在托管堆上。在这种情况下,我们不能直接使用内存转储命令,而需要借助一些辅助命令来转储值类型。
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,控制台程序输出:Press any key to continue(AddCoordinate),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们是人工中断程序的执行,当前的线程上下文是调试器的线程上下文不是托管线程上下文,我们必须切换到托管线程的上下文,执行命令【~0s】。
1 0:007> !bpmd ExampleCore_3_1_6 ExampleCore_3_1_6.ObjTypes.AddCoordinate 2 MethodDesc = 00007FF9EDAA0108 3 Adding pending breakpoints...
断点设置好后,我们继续执行调试器,执行命令【g】。
1 0:007> g 2 (3310.3864): CLR notification exception - code e0444143 (first chance) 3 JITTED ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate) 4 Setting breakpoint: bp 00007FF9ED9F1CE0 [ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate)] 5 Breakpoint 0 hit 6 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.AddCoordinate: 7 00007ff9`ed9f1ce0 55 push rbp
当前我们的调试器线程上下文也自动切换到托管线程的上下文,可以直接执行【!clrstack -a】命令。
1 0:000> !clrstack -a 2 OS Thread Id: 0x3864 (0) 3 Child SP IP Call Site 4 000000DEBDF7E968 00007ff9ed9f1ce0 ExampleCore_3_1_6.ObjTypes.AddCoordinate(Coordinate) [E:\Visual Studio\.\ExampleCore_3_1_6\Program.cs @ 48] 5 PARAMETERS: 6 this (<CLR reg>) = 0x0000027eb4409ce0 7 coord (<CLR reg>) = 0x000000debdf7e998 8 9 000000DEBDF7E970 00007ff9ed9f1a11 ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\..\ExampleCore_3_1_6\Program.cs @ 31] 10 PARAMETERS: 11 args (0x000000DEBDF7EA40) = 0x0000027eb4408ea0 12 LOCALS: 13 0x000000DEBDF7EA20 = 0x0000006400000064 14 0x000000DEBDF7EA18 = 0x0000027eb4409ce0 15 0x000000DEBDF7EA10 = 0x0000000000000000
0x0000027eb4409ce0 红色标注的是 this 指针。this 指针指向当前的对象实例。我们使用【!dumpobj】命令查看一下 this 指针。
1 0:000> !dumpobj /d 0x0000027eb4409ce0 2 Name: ExampleCore_3_1_6.ObjTypes 3 MethodTable: 00007ff9edaa0238 4 EEClass: 00007ff9eda8fca8 5 Tracked Type: false 6 Size: 48(0x30) bytes 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff9edaa01c8 4000001 18 ...jTypes+Coordinate 1 instance 0000027eb4409cf8 coordinate 11 00007ff9ed9d9df8 4000002 8 System.Int32[] 0 instance 0000027eb4409d10 intArray 12 00007ff9eda7fac8 4000003 10 System.String[] 0 instance 0000027eb4409d80 strArray
Fields 是最重要的信息,它包含了这个对象的元数据。各个域的表示信息可以看下表。
如果要显示这个域本身的内容,有两种方式。其中一种是使用【dp】命令,参数是引用类型实例的地址(000001d9b1c09ce0)和偏移(0x18)。
1 0:000> dp 000001d9b1c09ce0+0x18 2 000001d9`b1c09cf8 00000000`00000000 00000000`00000000 3 000001d9`b1c09d08 00000000`00000000 00007ff9`ebf49df8 4 000001d9`b1c09d18 00000000`00000005 00000002`00000001 5 000001d9`b1c09d28 00000004`00000003 00000000`00000005 6 000001d9`b1c09d38 00000000`00000000 00007ff9`ec064468 7 000001d9`b1c09d48 00000000`00000000 00000000`00000000 8 000001d9`b1c09d58 00000000`00000000 00000000`00000000 9 000001d9`b1c09d68 00000000`00000000 00007ff9`ec063de0
如果我们想查看【...jTypes+Coordinate】的具体的值,可以执行命令【!dumpvc mt addr】。
1 0:000> !dumpvc 00007ff9edaa01c8 0000027eb4409cf8 2 Name: ExampleCore_3_1_6.ObjTypes+Coordinate 3 MethodTable: 00007ff9edaa01c8 4 EEClass: 00007ff9eda8fd20 5 Size: 32(0x20) bytes 6 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 7 Fields: 8 MT Field Offset Type VT Attr Value Name 9 00007ff9ed961188 4000005 0 System.Int32 1 instance 0 xCord 10 00007ff9ed961188 4000006 4 System.Int32 1 instance 0 yCord 11 00007ff9ed961188 4000007 8 System.Int32 1 instance 0 zCord
4.1.3、引用类型的转储
A、基础知识
如果我们想将引用类型转储输出,很简单,直接使用【!dumpobj】命令就可以了。【!dumpobj】命令的参数可以直接跟引用类型的地址,如果不想输出域的内容,可以使用 -nofields 命令开关。这个命令也有一个简写形式就是【!do】。
命令格式:!DumpObj [-nofields] <object address>
其中 object address 是转储的对象地址。在默认情况下,【!DumpObj】 会转储出类型的信息及其相关的域。如果只需要一般性的信息,那么可以使用 -nofields 选项,它不输出域信息。
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】命令,运行调试器,等到调试器暂停,我们按【ctr+c】组合键进入中断模式。
由于我们手动中断调试器,需要执行线程上下文的切换,切换到托管线程的上下文,执行命令【~os】。
1 0:002> ~0s 2 coreclr!LookupMap<MethodTable *>::GetValueAt+0x3 [inlined in coreclr!ClassLoader::LoadTypeDefThrowing+0x89]: 3 00007ffa`536a5c49 48f7d2 not rdx
现在就可以使用【!clrstack -a】命令查看托管线程栈了。
1 0:000> !clrstack -a 2 OS Thread Id: 0x290c (0) 3 Child SP IP Call Site 4 00000068AEBEDE68 00007ffa536a5c49 [ExternalMethodFrame: 00000068aebede68] 5 。。。。。。(省略了) 6 00000068AEBEE650 00007FF9F3C51987 ExampleCore_3_1_7.Program.Main(System.String[]) 7 PARAMETERS: 8 args (0x00000068AEBEE6A0) = 0x000001afc2408ea0 9 LOCALS: 10 0x00000068AEBEE688 = 0x000001afc2409640
0x000001afc2409640 红色标注的就是 Person 类型的局部变量 person。
废话不多说,直接使用【!dumpobj】或者【!do】命令查看 Person 的详情了。
1 0:000> !dumpobj 0x000001afc2409640 2 Name: ExampleCore_3_1_7.Person 3 MethodTable: 00007ff9f3d093e0 4 EEClass: 00007ff9f3d11f18 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 00007ff9f3bc1188 4000001 10 System.Int32 1 instance 20 Age 11 00007ff9f3c3ec08 4000002 8 System.String 0 instance 000001f0546704a0 Name 12 13 14 0:000> !do 0x000001afc2409640 15 Name: ExampleCore_3_1_7.Person 16 MethodTable: 00007ff9f3d093e0 17 EEClass: 00007ff9f3d11f18 18 Tracked Type: false 19 Size: 32(0x20) bytes 20 File: E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll 21 Fields: 22 MT Field Offset Type VT Attr Value Name 23 00007ff9f3bc1188 4000001 10 System.Int32 1 instance 20 Age 24 00007ff9f3c3ec08 4000002 8 System.String 0 instance 000001f0546704a0 Name
如果不想输出【Fields】的内容,可以使用 -nofields 命令开关。
1 0:000> !dumpobj -nofields 0x000001afc2409640 2 Name: ExampleCore_3_1_7.Person 3 MethodTable: 00007ff9f3d093e0 4 EEClass: 00007ff9f3d11f18 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 9 0:000> !do -nofields 0x000001afc2409640 10 Name: ExampleCore_3_1_7.Person 11 MethodTable: 00007ff9f3d093e0 12 EEClass: 00007ff9f3d11f18 13 Tracked Type: false 14 Size: 32(0x20) bytes 15 File: E:\Visual Studio 2022\...ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll 16 0:000>
2)、使用【Windbg Preview】 调试
调试源码:ExampleCore_3_1_7
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断了调试器,当前的线程上下文是调试器的,不是托管线程的,我们必须使用【~0s】命令,切换到托管线程。
1 0:006> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffb`2fdaae74 c3 ret
查看当前的托管线程的调用栈,使用【!clrstack -a】命令。
1 0:000> !clrstack -a 2 OS Thread Id: 0x99c (0) 3 Child SP IP Call Site 4 0000006E2117E200 00007ffb2fdaae54 [InlinedCallFrame: 0000006e2117e200] 5 0000006E2117E200 00007ffaeadb76eb [InlinedCallFrame: 0000006e2117e200] 6 。。。。。。(省略了) 7 8 0000006E2117E550 00007ff9f3461987 ExampleCore_3_1_7.Program.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_7\Program.cs @ 8] 9 PARAMETERS: 10 args (0x0000006E2117E5A0) = 0x000001da19808ea0 11 LOCALS: 12 0x0000006E2117E588 = 0x000001da19809640
0x000001da19809640 这个地址就是引用类型 Person 的局部变量 person。我们可以直接使用【!dumpobj】或者【!do】命令。
1 0:000> !DumpObj /d 000001da19809640 2 Name: ExampleCore_3_1_7.Person 3 MethodTable: 00007ff9f35193e0 4 EEClass: 00007ff9f3521f18 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 00007ff9f33d1188 4000001 10 System.Int32 1 instance 20 Age 11 00007ff9f344ec08 4000002 8 System.String 0 instance 0000021aab9904a0 Name 12 13 14 0:000> !do 000001da19809640 15 Name: ExampleCore_3_1_7.Person 16 MethodTable: 00007ff9f35193e0 17 EEClass: 00007ff9f3521f18 18 Tracked Type: false 19 Size: 32(0x20) bytes 20 File: E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll 21 Fields: 22 MT Field Offset Type VT Attr Value Name 23 00007ff9f33d1188 4000001 10 System.Int32 1 instance 20 Age 24 00007ff9f344ec08 4000002 8 System.String 0 instance 0000021aab9904a0 Name
我们可以使用 -nofields 命令开关去掉域的内容。
1 0:000> !DumpObj -nofields 000001da19809640 2 Name: ExampleCore_3_1_7.Person 3 MethodTable: 00007ff9f35193e0 4 EEClass: 00007ff9f3521f18 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 9 10 0:000> !do -nofields 000001da19809640 11 Name: ExampleCore_3_1_7.Person 12 MethodTable: 00007ff9f35193e0 13 EEClass: 00007ff9f3521f18 14 Tracked Type: false 15 Size: 32(0x20) bytes 16 File: E:\Visual Studio 2022\...\ExampleCore_3_1_7\bin\Debug\net8.0\ExampleCore_3_1_7.dll
4.1.4、数组的转储
A、基础知识
CLR 将数组作为第一级(first class)引用类型(因为所有数组都是从 System.Array 继承下来的),因此我们可以直接使用【!dumpobj】命令或者【!dumparray】命令转储出数组的在内存中的内容。
数组本身是引用类型,但是它的元素是可以区分为:值类型和引用类型的。由此,我们可以把数组称为值类型的数组和引用类型的数组。虽然有这个区别,但是它们的内存布局是一致的,只不过在查看元素的时候有区别,如果是值类型的元素,可以使用【!dumpvc】命令,如果是引用类型,可以使用【!dumpobj】命令查看。
数组的内存布局如图:
转储数组不光可以使用【!DumpObj】命令,当然也可以使用【dp】命令,直接显示内存数据,还有一个命令可以直接显示数组的数据,这个命令就是【!DumpArray】。该命令的格式:!DumpArray [-start <startIndex>] [-length <length>] [-details] [-nofields] <array object address>
-start 选项能控制从数组中的哪个索引开始,而-length 选项则控制显示数组中多少个元素。例如,如果希望转储出位于地址X上的数组的前三个元素,并从索引2开始,那么可以使用以下命令行:!DumpArray -start 2 -length 3 X。
如果增加 -details 选项,【!DumpArray】会输出更为详细的信息,在数组中的每个元素上都执行【!DumpObj】和【!DumpVC】命令。最后,-nofields 选项能使得 DumpArray 在与 -detail 选项一起使用的情况下不输出与域相关的信息。
这里的概念不多,但是,要演示的内容很多,而且,内容很简单,就不多说了。
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】调试器窗口。
继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Arrays),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。
我们进入中断模式后,需要切换线程到托管线程,执行命令【~0s】。
1 0:002> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`eeaeae74 c3 ret
现在,我们就可以查看托管线程栈了,执行命令【!clrstack -a】。
0:000> !clrstack -a OS Thread Id: 0x3394 (0) Child SP IP Call Site 00000090F577E5E8 00007ffaeeaeae74 [InlinedCallFrame: 00000090f577e5e8] 00000090F577E5E8 00007ffadb81787a [InlinedCallFrame: 00000090f577e5e8] 。。。。。。(省略了) 00000090F577E770 00007FF9BE1E1A2D ExampleCore_3_1_6.ObjTypes.Main(System.String[]) PARAMETERS: args (0x00000090F577E840) = 0x000002282e008ea0 LOCALS: 0x00000090F577E820 = 0x0000006400000064 0x00000090F577E818 = 0x000002282e009ce0 0x00000090F577E810 = 0x0000000000000000
0x000002282e009ce0 红色标注的就是 ObjTypes 类型的指针,我们可以使用【!do】命令查看它。
1 0:000> !do 0x000002282e009ce0 2 Name: ExampleCore_3_1_6.ObjTypes 3 MethodTable: 00007ff9be290238 4 EEClass: 00007ff9be27fca8 5 Tracked Type: false 6 Size: 48(0x30) bytes 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff9be2901c8 4000001 18 ...jTypes+Coordinate 1 instance 000002282e009cf8 coordinate 11 00007ff9be1c9df8 4000002 8 System.Int32[] 0 instance 000002282e009d10 intArray 12 00007ff9be26fac8 4000003 10 System.String[] 0 instance 000002282e009d80 strArray 13 0:000>
这里我们开始关注 intArray 值类型数组,也就是红色标注的。intArray 数组的地址是:000002282e009d10,有了地址,我们就可以使用【!dumpobj 000002282e009d10】命令转储出详情。
1 0:000> !dumpobj 000002282e009d10 2 Name: System.Int32[] 3 MethodTable: 00007ff9be1c9df8 4 EEClass: 00007ff9be1c9d78 5 Tracked Type: false 6 Size: 44(0x2c) bytes 7 Array: Rank 1, Number of elements 5, Type Int32 8 Fields: 9 None
验证了我们的说法,输出了数组的名称,维度(Rank),元素个数(Number of elements)等。我们依然可以使用【dp 000002282e009d10】命令输出详情。
我们使用【!dumpmt 00007ff9`be1c9df8】命令是不是数组的方法表。
1 0:000> !dumpmt 00007ff9`be1c9df8 2 EEClass: 00007FF9BE1C9D78 3 Module: 00007FF9BE084000 4 Name: System.Int32[] 5 mdToken: 0000000002000000 6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 7 BaseSize: 0x18 8 ComponentSize: 0x4 9 DynamicStatics: false 10 ContainsPointers false 11 Slots in VTable: 28 12 Number of IFaces in IFaceMap: 6
当然,我们可以直接使用【!dumparray 000002282e009d10】命令查看 intArray 数组详情,这个命令更直观。
1 0:000> !dumparray 000002282e009d10 2 Name: System.Int32[] 3 MethodTable: 00007ff9be1c9df8 4 EEClass: 00007ff9be1c9d78 5 Size: 44(0x2c) bytes 6 Array: Rank 1, Number of elements 5, Type Int32 7 Element Methodtable: 00007ff9be151188 8 [0] 000002282e009d20 9 [1] 000002282e009d24 10 [2] 000002282e009d28 11 [3] 000002282e009d2c 12 [4] 000002282e009d30 13 14 0:000> !dumpvc 00007ff9be151188 000002282e009d20 15 Name: System.Int32 16 MethodTable: 00007ff9be151188 17 EEClass: 00007ff9be141e20 18 Size: 24(0x18) bytes 19 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 20 Fields: 21 MT Field Offset Type VT Attr Value Name 22 00007ff9be151188 400051d 0 System.Int32 1 instance 1 m_value 23 24 0:000> !dumpvc 00007ff9be151188 000002282e009d24 25 Name: System.Int32 26 MethodTable: 00007ff9be151188 27 EEClass: 00007ff9be141e20 28 Size: 24(0x18) bytes 29 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 30 Fields: 31 MT Field Offset Type VT Attr Value Name 32 00007ff9be151188 400051d 0 System.Int32 1 instance 2 m_value 33 34 0:000> !dumpvc 00007ff9be151188 000002282e009d28 35 Name: System.Int32 36 MethodTable: 00007ff9be151188 37 EEClass: 00007ff9be141e20 38 Size: 24(0x18) bytes 39 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 40 Fields: 41 MT Field Offset Type VT Attr Value Name 42 00007ff9be151188 400051d 0 System.Int32 1 instance 3 m_value 43 44 0:000> !dumpvc 00007ff9be151188 000002282e009d2c 45 Name: System.Int32 46 MethodTable: 00007ff9be151188 47 EEClass: 00007ff9be141e20 48 Size: 24(0x18) bytes 49 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 50 Fields: 51 MT Field Offset Type VT Attr Value Name 52 00007ff9be151188 400051d 0 System.Int32 1 instance 4 m_value 53 54 0:000> !dumpvc 00007ff9be151188 000002282e009d30 55 Name: System.Int32 56 MethodTable: 00007ff9be151188 57 EEClass: 00007ff9be141e20 58 Size: 24(0x18) bytes 59 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 60 Fields: 61 MT Field Offset Type VT Attr Value Name 62 00007ff9be151188 400051d 0 System.Int32 1 instance 5 m_value 63 0:000>
编译项目,打开【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】调试器窗口。
继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Arrays),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。
1 0:002> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`eeaeae74 c3 ret
现在,我们就可以查看托管线程栈了,执行命令【!clrstack -a】。
1 0:000> !clrstack -a 2 OS Thread Id: 0x3394 (0) 3 Child SP IP Call Site 4 00000090F577E5E8 00007ffaeeaeae74 [InlinedCallFrame: 00000090f577e5e8] 5 00000090F577E5E8 00007ffadb81787a [InlinedCallFrame: 00000090f577e5e8] 6 。。。。。。(省略了) 7 8 00000090F577E770 00007FF9BE1E1A2D ExampleCore_3_1_6.ObjTypes.Main(System.String[]) 9 PARAMETERS: 10 args (0x00000090F577E840) = 0x000002282e008ea0 11 LOCALS: 12 0x00000090F577E820 = 0x0000006400000064 13 0x00000090F577E818 = 0x000002282e009ce0 14 0x00000090F577E810 = 0x0000000000000000
0x000002282e009ce0 红色标注的就是 ObjTypes 类型的指针,我们可以使用【!do】命令查看它。
1 0:000> !do 0x000002282e009ce0 2 Name: ExampleCore_3_1_6.ObjTypes 3 MethodTable: 00007ff9be290238 4 EEClass: 00007ff9be27fca8 5 Tracked Type: false 6 Size: 48(0x30) bytes 7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff9be2901c8 4000001 18 ...jTypes+Coordinate 1 instance 000002282e009cf8 coordinate 11 00007ff9be1c9df8 4000002 8 System.Int32[] 0 instance 000002282e009d10 intArray 12 00007ff9be26fac8 4000003 10 System.String[] 0 instance 000002282e009d80 strArray
这里我们开始关注 strArray 引用类型数组,也就是红色标注的。我们先使用【dp 000002282e009d80】命令转储一下。
执行【!dumpmt 00007ff9`be26fac8】命令查看 strArray 数组的方法表。
1 0:000> !dumpmt 00007ff9`be26fac8 2 EEClass: 00007FF9BE11C440 3 Module: 00007FF9BE084000 4 Name: System.String[] 5 mdToken: 0000000002000000 6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 7 BaseSize: 0x18 8 ComponentSize: 0x8 9 DynamicStatics: false 10 ContainsPointers true 11 Slots in VTable: 28 12 Number of IFaces in IFaceMap: 6
我们可以使用【!dumpobj】命令查看查看元素的具体信息。
1 0:000> !do -nofields 00000268`c0280b78 2 Name: System.String 3 MethodTable: 00007ff9be1cec08 4 EEClass: 00007ff9be1aa500 5 Tracked Type: false 6 Size: 36(0x24) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: Welcome 9 10 0:000> !do -nofields 00000268`c0280ba0 11 Name: System.String 12 MethodTable: 00007ff9be1cec08 13 EEClass: 00007ff9be1aa500 14 Tracked Type: false 15 Size: 26(0x1a) bytes 16 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 17 String: to 18 19 0:000> !do -nofields 00000268`c0280bc0 20 Name: System.String 21 MethodTable: 00007ff9be1cec08 22 EEClass: 00007ff9be1aa500 23 Tracked Type: false 24 Size: 38(0x26) bytes 25 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 26 String: Advanced 27 28 0:000> !do -nofields 00000268`c0280be8 29 Name: System.String 30 MethodTable: 00007ff9be1cec08 31 EEClass: 00007ff9be1aa500 32 Tracked Type: false 33 Size: 30(0x1e) bytes 34 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 35 String: .NET 36 37 0:000> !do -nofields 00000268`c0280c08 38 Name: System.String 39 MethodTable: 00007ff9be1cec08 40 EEClass: 00007ff9be1aa500 41 Tracked Type: false 42 Size: 40(0x28) bytes 43 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 44 String: Debugging
当然,还有一个更容易,更直接的命令,就是【!dumparray】可以输出所有数组的信息。
内容很简单,就不赘述了。
1 0:000> !do -nofields 00000268c0280b78 2 Name: System.String 3 MethodTable: 00007ff9be1cec08 4 EEClass: 00007ff9be1aa500 5 Tracked Type: false 6 Size: 36(0x24) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: Welcome
可以继续使用【!do】命令查看元素的内容,这里就不演示了。
2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_6
2.1)、值类型数组
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Arrays),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。
1 0:001> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`eeaeae74 c3 ret
现在我们需要查看一下托管线程的调用栈,执行命令【!clrstack -a】。
1 0:000> !clrstack -a 2 OS Thread Id: 0x3144 (0) 3 Child SP IP Call Site 4 0000000C91F7E668 00007ffaeeaeae74 [InlinedCallFrame: 0000000c91f7e668] 5 0000000C91F7E668 00007ffa375c787a [InlinedCallFrame: 0000000c91f7e668] 6 。。。。。。(省略了) 7 8 0000000C91F7E7F0 00007ff9b6fa1a2d ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 36] 9 PARAMETERS: 10 args (0x0000000C91F7E8C0) = 0x000001cfe3408ea0 11 LOCALS: 12 0x0000000C91F7E8A0 = 0x0000006400000064 13 0x0000000C91F7E898 = 0x000001cfe3409ce0 14 0x0000000C91F7E890 = 0x0000000000000000
0x000001cfe3409ce0 标红的就是 ObjTypes 类型的指针,执行命令【!do 0x000001cfe3409ce0】确认一下。
1 0:000> !do 0x000001cfe3409ce0 2 Name: ExampleCore_3_1_6.ObjTypes 3 MethodTable: 00007ff9b7050238 4 EEClass: 00007ff9b703fca8 5 Tracked Type: false 6 Size: 48(0x30) bytes 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff9b70501c8 4000001 18 ...jTypes+Coordinate 1 instance 000001cfe3409cf8 coordinate 11 00007ff9b6f89df8 4000002 8 System.Int32[] 0 instance 000001cfe3409d10 intArray 12 00007ff9b702fac8 4000003 10 System.String[] 0 instance 000001cfe3409d80 strArray
我们看到了 ExampleCore_3_1_6.ObjTypes 类型有两个数组的域,分别是:intArray 和 strArray。我们获取了 intArray数组的地址:000001cfe3409d10 ,我们使用【!dumpobj 000001cfe3409d10】命令查看一下 intArray 数组的详情。
1 0:000> !dumpobj 000001cfe3409d10 2 Name: System.Int32[] 3 MethodTable: 00007ff9b6f89df8 4 EEClass: 00007ff9b6f89d78 5 Tracked Type: false 6 Size: 44(0x2c) bytes 7 Array: Rank 1, Number of elements 5, Type Int32 (Print Array) 8 Fields: 9 None
【!dumpobj】命令输出了数组的名称(Name),方法表(MethodTable),数组的维度(Rank),数组的类型(Type)和数组的元素个数(Number of elements),但是我们没有看到数组的元素内容。如果想查看数组的内容,我们可以使用【dp】命令。
1 0:000> dp 000001cfe3409d10 2 000001cf`e3409d10 00007ff9`b6f89df8 00000000`00000005 3 000001cf`e3409d20 00000002`00000001 00000004`00000003 4 000001cf`e3409d30 00000000`00000005 00000000`00000000 5 000001cf`e3409d40 00007ff9`b70a4468 00000000`00000000 6 000001cf`e3409d50 00000000`00000000 00000000`00000000 7 000001cf`e3409d60 00000000`00000000 00000000`00000000 8 000001cf`e3409d70 00007ff9`b70a3de0 00000000`00000000 9 000001cf`e3409d80 00007ff9`b702fac8 00000000`00000005
00007ff9`b6f89df8 第一个值就是数组本身的方法表的地址。00000000`00000005 表示的数组元素个数,是 5 个元素。00000002`00000001、00000004`00000003、00000000`00000005 就是数组的元素内容,分别是:1,2,3,4,5。
00007ff9`b6f89df8 这个地址是数组本身的方法表,我们可以执行【!dumpmt 00007ff9`b6f89df8】命令验证一下。
1 0:000> !dumpmt 00007ff9`b6f89df8 2 EEClass: 00007ff9b6f89d78 3 Module: 00007ff9b6e44000 4 Name: System.Int32[] 5 mdToken: 0000000002000000 6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet. 8 BaseSize: 0x18 9 ComponentSize: 0x4 10 DynamicStatics: false 11 ContainsPointers: false 12 Slots in VTable: 28 13 Number of IFaces in IFaceMap: 6
2.2)、引用类型数组
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Arrays),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。
1 0:001> ~0s
2 ntdll!NtDeviceIoControlFile+0x14:
3 00007ffa`eeaeae74 c3 ret
现在我们需要查看一下托管线程的调用栈,执行命令【!clrstack -a】。
1 0:000> !clrstack -a
2 OS Thread Id: 0x3144 (0)
3 Child SP IP Call Site
4 0000000C91F7E668 00007ffaeeaeae74 [InlinedCallFrame: 0000000c91f7e668]
5 0000000C91F7E668 00007ffa375c787a [InlinedCallFrame: 0000000c91f7e668]
6 。。。。。。(省略了)
7
8 0000000C91F7E7F0 00007ff9b6fa1a2d ExampleCore_3_1_6.ObjTypes.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_6\Program.cs @ 36]
9 PARAMETERS:
10 args (0x0000000C91F7E8C0) = 0x000001cfe3408ea0
11 LOCALS:
12 0x0000000C91F7E8A0 = 0x0000006400000064
13 0x0000000C91F7E898 = 0x000001cfe3409ce0
14 0x0000000C91F7E890 = 0x0000000000000000
0x000001cfe3409ce0 标红的就是 ObjTypes 类型的指针,执行命令【!do 0x000001cfe3409ce0】确认一下。
1 0:000> !do 0x000001cfe3409ce0
2 Name: ExampleCore_3_1_6.ObjTypes
3 MethodTable: 00007ff9b7050238
4 EEClass: 00007ff9b703fca8
5 Tracked Type: false
6 Size: 48(0x30) bytes
7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff9b70501c8 4000001 18 ...jTypes+Coordinate 1 instance 000001cfe3409cf8 coordinate
11 00007ff9b6f89df8 4000002 8 System.Int32[] 0 instance 000001cfe3409d10 intArray
12 00007ff9b702fac8 4000003 10 System.String[] 0 instance 000001cfe3409d80 strArray
1 0:000> !dumpobj /d 000001cfe3409d80 2 Name: System.String[] 3 MethodTable: 00007ff9b702fac8 4 EEClass: 00007ff9b6edc440 5 Tracked Type: false 6 Size: 64(0x40) bytes 7 Array: Rank 1, Number of elements 5, Type CLASS (Print Array) 8 Fields: 9 None
我们也可以使用【dp 000001cfe3409d80】命令查看它的显示。
0:000> dp 000001cfe3409d80 000001cf`e3409d80 00007ff9`b702fac8 00000000`00000005 000001cf`e3409d90 00000210`75660b78 00000210`75660ba0 000001cf`e3409da0 00000210`75660bc0 00000210`75660be8 000001cf`e3409db0 00000210`75660c08 00000000`00000000 000001cf`e3409dc0 00007ff9`b6f11188 00000000`00000064 000001cf`e3409dd0 00000000`00000000 00007ff9`b6f11188 000001cf`e3409de0 00000000`00000064 00000000`00000000 000001cf`e3409df0 00007ff9`b6f11188 00000000`00000064
00007ff9`b702fac8 就是 strArray 数组的方法表,我们可以使用【!dumpmt 00007ff9`b702fac8】验证这点。
1 0:000> !dumpmt 00007ff9`b702fac8 2 EEClass: 00007ff9b6edc440 3 Module: 00007ff9b6e44000 4 Name: System.String[] 5 mdToken: 0000000002000000 6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet. 8 BaseSize: 0x18 9 ComponentSize: 0x8 10 DynamicStatics: false 11 ContainsPointers: true 12 Slots in VTable: 28 13 Number of IFaces in IFaceMap: 6
【dp 000001cfe3409d80】命令的 00000000`00000005 这个值就是 strArray 数组的元素个数。【dp 000001cfe3409d80】命令的5个输出就是数组元素的值,由于它们是引用类型,我们可以依次使用【!dumpobj】命令输出它们的内容。
0:000> !dumpobj 00000210`75660b78 Name: System.String MethodTable: 00007ff9b6f8ec08 EEClass: 00007ff9b6f6a500 Tracked Type: false Size: 36(0x24) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll String: Welcome Fields: MT Field Offset Type VT Attr Value Name 00007ff9b6f11188 400033b 8 System.Int32 1 instance 7 _stringLength 00007ff9b6f1b538 400033c c System.Char 1 instance 57 _firstChar 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 0:000> !dumpobj 00000210`75660ba0 Name: System.String MethodTable: 00007ff9b6f8ec08 EEClass: 00007ff9b6f6a500 Tracked Type: false Size: 26(0x1a) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll String: to Fields: MT Field Offset Type VT Attr Value Name 00007ff9b6f11188 400033b 8 System.Int32 1 instance 2 _stringLength 00007ff9b6f1b538 400033c c System.Char 1 instance 74 _firstChar 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 0:000> !dumpobj 00000210`75660bc0 Name: System.String MethodTable: 00007ff9b6f8ec08 EEClass: 00007ff9b6f6a500 Tracked Type: false Size: 38(0x26) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll String: Advanced Fields: MT Field Offset Type VT Attr Value Name 00007ff9b6f11188 400033b 8 System.Int32 1 instance 8 _stringLength 00007ff9b6f1b538 400033c c System.Char 1 instance 41 _firstChar 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 0:000> !dumpobj 00000210`75660be8 Name: System.String MethodTable: 00007ff9b6f8ec08 EEClass: 00007ff9b6f6a500 Tracked Type: false Size: 30(0x1e) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll String: .NET Fields: MT Field Offset Type VT Attr Value Name 00007ff9b6f11188 400033b 8 System.Int32 1 instance 4 _stringLength 00007ff9b6f1b538 400033c c System.Char 1 instance 2e _firstChar 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 0:000> !dumpobj 00000210`75660c08 Name: System.String MethodTable: 00007ff9b6f8ec08 EEClass: 00007ff9b6f6a500 Tracked Type: false Size: 40(0x28) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll String: Debugging Fields: MT Field Offset Type VT Attr Value Name 00007ff9b6f11188 400033b 8 System.Int32 1 instance 9 _stringLength 00007ff9b6f1b538 400033c c System.Char 1 instance 44 _firstChar 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty
红色标注的就是数组元素具体的值。
上面的命令有些繁琐,我们可以直接使用【!dumparray】命令获取我们想要的数据。
我们直接输出 intArray,使用【!dumparray】命令。
1 0:000> !dumparray 000001cfe3409d10 2 Name: System.Int32[] 3 MethodTable: 00007ff9b6f89df8 4 EEClass: 00007ff9b6f89d78 5 Size: 44(0x2c) bytes 6 Array: Rank 1, Number of elements 5, Type Int32 7 Element Methodtable: 00007ff9b6f11188 8 [0] 000001cfe3409d20 9 [1] 000001cfe3409d24 10 [2] 000001cfe3409d28 11 [3] 000001cfe3409d2c 12 [4] 000001cfe3409d30 13 14 0:000> !DumpVC /d 00007ff9b6f11188 000001cfe3409d20 15 Name: System.Int32 16 MethodTable: 00007ff9b6f11188 17 EEClass: 00007ff9b6f01e20 18 Size: 24(0x18) bytes 19 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 20 Fields: 21 MT Field Offset Type VT Attr Value Name 22 00007ff9b6f11188 400051d 0 System.Int32 1 instance 1 m_value 23 24 0:000> !DumpVC /d 00007ff9b6f11188 000001cfe3409d24 25 Name: System.Int32 26 MethodTable: 00007ff9b6f11188 27 EEClass: 00007ff9b6f01e20 28 Size: 24(0x18) bytes 29 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 30 Fields: 31 MT Field Offset Type VT Attr Value Name 32 00007ff9b6f11188 400051d 0 System.Int32 1 instance 2 m_value 33 34 0:000> !DumpVC /d 00007ff9b6f11188 000001cfe3409d28 35 Name: System.Int32 36 MethodTable: 00007ff9b6f11188 37 EEClass: 00007ff9b6f01e20 38 Size: 24(0x18) bytes 39 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 40 Fields: 41 MT Field Offset Type VT Attr Value Name 42 00007ff9b6f11188 400051d 0 System.Int32 1 instance 3 m_value 43 44 0:000> !DumpVC /d 00007ff9b6f11188 000001cfe3409d2c 45 Name: System.Int32 46 MethodTable: 00007ff9b6f11188 47 EEClass: 00007ff9b6f01e20 48 Size: 24(0x18) bytes 49 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 50 Fields: 51 MT Field Offset Type VT Attr Value Name 52 00007ff9b6f11188 400051d 0 System.Int32 1 instance 4 m_value 53 54 0:000> !DumpVC /d 00007ff9b6f11188 000001cfe3409d30 55 Name: System.Int32 56 MethodTable: 00007ff9b6f11188 57 EEClass: 00007ff9b6f01e20 58 Size: 24(0x18) bytes 59 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 60 Fields: 61 MT Field Offset Type VT Attr Value Name 62 00007ff9b6f11188 400051d 0 System.Int32 1 instance 5 m_value
我们直接输出strArray,使用【!dumparray】命令。
1 0:000> !dumparray 000001cfe3409d80 2 Name: System.String[] 3 MethodTable: 00007ff9b702fac8 4 EEClass: 00007ff9b6edc440 5 Size: 64(0x40) bytes 6 Array: Rank 1, Number of elements 5, Type CLASS 7 Element Methodtable: 00007ff9b6f8ec08 8 [0] 0000021075660b78 9 [1] 0000021075660ba0 10 [2] 0000021075660bc0 11 [3] 0000021075660be8 12 [4] 0000021075660c08 13 14 0:000> !DumpObj /d 0000021075660b78 15 Name: System.String 16 MethodTable: 00007ff9b6f8ec08 17 EEClass: 00007ff9b6f6a500 18 Tracked Type: false 19 Size: 36(0x24) bytes 20 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 21 String: Welcome 22 Fields: 23 MT Field Offset Type VT Attr Value Name 24 00007ff9b6f11188 400033b 8 System.Int32 1 instance 7 _stringLength 25 00007ff9b6f1b538 400033c c System.Char 1 instance 57 _firstChar 26 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 27 28 0:000> !DumpObj /d 0000021075660ba0 29 Name: System.String 30 MethodTable: 00007ff9b6f8ec08 31 EEClass: 00007ff9b6f6a500 32 Tracked Type: false 33 Size: 26(0x1a) bytes 34 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 35 String: to 36 Fields: 37 MT Field Offset Type VT Attr Value Name 38 00007ff9b6f11188 400033b 8 System.Int32 1 instance 2 _stringLength 39 00007ff9b6f1b538 400033c c System.Char 1 instance 74 _firstChar 40 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 41 42 0:000> !DumpObj /d 0000021075660bc0 43 Name: System.String 44 MethodTable: 00007ff9b6f8ec08 45 EEClass: 00007ff9b6f6a500 46 Tracked Type: false 47 Size: 38(0x26) bytes 48 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 49 String: Advanced 50 Fields: 51 MT Field Offset Type VT Attr Value Name 52 00007ff9b6f11188 400033b 8 System.Int32 1 instance 8 _stringLength 53 00007ff9b6f1b538 400033c c System.Char 1 instance 41 _firstChar 54 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 55 56 0:000> !DumpObj /d 0000021075660be8 57 Name: System.String 58 MethodTable: 00007ff9b6f8ec08 59 EEClass: 00007ff9b6f6a500 60 Tracked Type: false 61 Size: 30(0x1e) bytes 62 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 63 String: .NET 64 Fields: 65 MT Field Offset Type VT Attr Value Name 66 00007ff9b6f11188 400033b 8 System.Int32 1 instance 4 _stringLength 67 00007ff9b6f1b538 400033c c System.Char 1 instance 2e _firstChar 68 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty 69 70 0:000> !DumpObj /d 0000021075660c08 71 Name: System.String 72 MethodTable: 00007ff9b6f8ec08 73 EEClass: 00007ff9b6f6a500 74 Tracked Type: false 75 Size: 40(0x28) bytes 76 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 77 String: Debugging 78 Fields: 79 MT Field Offset Type VT Attr Value Name 80 00007ff9b6f11188 400033b 8 System.Int32 1 instance 9 _stringLength 81 00007ff9b6f1b538 400033c c System.Char 1 instance 44 _firstChar 82 00007ff9b6f8ec08 400033a c8 System.String 0 static 0000021075660008 Empty
4.1.5、栈上对象的转储
A、基础知识
每个在 CLR 上下文中运行的托管线程都有一组相关数据。除了 CLR 维护的记录数据外,每个线程还包含了一些基本的信息,这些信息使CLR 能够维持栈和参数以及局部变量的完整性。在大多数时候,我们可以使用【!clrstack】命令找出每个栈帧的局部变量和参数,但是有时候需要对栈进行更深入的分析,就可以使用【!dumpstackobjects】命令。它能对栈进行遍历,并输出栈上所有的托管对象。
命令格式:!DumpStackObjects [-verify] [top stack [bottom stack]],
如果没有指定任何参数,那么 DumpStackObjects 会输出当前线程的所有托管对象。
verify 选项表示对找到的每个托管对象进行一个验证过程,这对对象是否被破坏来说非常有用。
如果想对这个命令的输出进行限制,需要指定一个范围(栈顶[top stack]和栈低[bottom stack])。
这个命令确实有点长,当然也有一个缩写形式:dso,可以直接执行命令【!dso】。
B、眼见为实
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_6
编译项目,打开【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】调试器窗口。
继续使用【g】命令运行调试器,直到调试器输出:Press any key to continue(Generics),然后,我们手动【ctrl+c】进入调试器的中断模式,就可以调试了。
我们进入中断模式后,需要切换线程到托管线程,执行命令【~0s】。
1 0:002> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`eeaeae74 c3 ret
我们直接执行【!dso】或者【!dumpstackobjects】命令。
1 0:000> !dso 2 OS Thread Id: 0x1d8c (0) 3 RSP/REG Object Name 4 000000F45E99E8A8 00000235eb220b30 Interop+INPUT_RECORD 5 000000F45E99E918 00000235eb220b30 Interop+INPUT_RECORD 6 000000F45E99E920 000001f559009c98 System.Object 7 000000F45E99E950 00000235eb220560 System.String Press any key to continue(Generics) 8 000000F45E99E9B8 000001f559009c38 System.IO.TextWriter+SyncTextWriter 9 000000F45E99E9C0 000001f559009ce0 ExampleCore_3_1_6.ObjTypes 10 000000F45E99EA28 000001f559009ce0 ExampleCore_3_1_6.ObjTypes 11 000000F45E99EA48 000001f559009ce0 ExampleCore_3_1_6.ObjTypes 12 000000F45E99EA70 000001f559008ea0 System.String[] 13 000000F45E99EB18 000001f559008ea0 System.String[] 14 000000F45E99ED10 000001f559008ea0 System.String[] 15 000000F45E99ED18 000001f559008ea0 System.String[] 16 000000F45E99EE30 000001f559008ea0 System.String[] 17 000000F45E99EEB0 000001f559008eb8 System.String E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 18 000000F45E99EEC0 000001f559008ea0 System.String[] 19 000000F45E99EED0 000001f559008e80 System.String[] 20 000000F45E99EF08 000001f559008eb8 System.String E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 21 000000F45E99F0B8 000001f559008ea0 System.String[]
在输出结果中有 3 列:
SP/REG:该列表式托管对象所在的栈地址。
Object:是托管对象的地址。我们可以使用【!dumpobj】命令查看对象的详情。
Name:该列表示托管对象的名称。
在输出的内容中,我们看到有几行是完全相同的,这也在预料之中,因为对象可能从一个函数传递另一个函数,其中每个栈帧都包含了对同一个对象的引用。
调试源码:ExampleCore_3_1_6
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:Press any key to continue(Generics),我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
由于我们手动中断调试器的执行,所以需要切换到托管线程的上下文中,执行命令【~0s】。
1 0:007> ~0s 2 ntdll!NtDeviceIoControlFile+0x14: 3 00007ffa`eeaeae74 c3 ret
直接执行命令【!DumpStackObjects】。
1 0:000> !DumpStackObjects 2 OS Thread Id: 0x3a04 (0) 3 SP/REG Object Name 4 00c21e57e918 026cd5cb1378 System.String 5 00c21e57ea28 026cd5cb0b30 Interop+INPUT_RECORD 6 00c21e57ea98 026cd5cb0b30 Interop+INPUT_RECORD 7 00c21e57eaa0 022c43c09c98 System.Object 8 00c21e57ead0 026cd5cb0560 System.String 9 00c21e57eb38 022c43c09c38 System.IO.TextWriter+SyncTextWriter 10 00c21e57eb40 022c43c09ce0 ExampleCore_3_1_6.ObjTypes 11 00c21e57eba8 022c43c09ce0 ExampleCore_3_1_6.ObjTypes 12 00c21e57ebc8 022c43c09ce0 ExampleCore_3_1_6.ObjTypes 13 00c21e57ebf0 022c43c08ea0 System.String[] 14 00c21e57ec98 022c43c08ea0 System.String[] 15 00c21e57ee90 022c43c08ea0 System.String[] 16 00c21e57ee98 022c43c08ea0 System.String[] 17 00c21e57efb0 022c43c08ea0 System.String[] 18 00c21e57f030 022c43c08eb8 System.String 19 00c21e57f040 022c43c08ea0 System.String[] 20 00c21e57f050 022c43c08e80 System.String[] 21 00c21e57f088 022c43c08eb8 System.String 22 00c21e57f238 022c43c08ea0 System.String[]
在输出结果中有 3 列:
SP/REG:该列表式托管对象所在的栈地址。
Object:是托管对象的地址。我们可以使用【!dumpobj】命令查看对象的详情。
Name:该列表示托管对象的名称。
在输出的内容中,我们看到有几行是完全相同的,这也在预料之中,因为对象可能从一个函数传递另一个函数,其中每个栈帧都包含了对同一个对象的引用。
4.1.6、找出对象的大小
A、基础知识
我们能找到一个对象的准确大小,对于我们排查一些问题是很有用的。我们知道【!dumpobj】命令可以输出一个对象的大小。如图:
这种方法对我们找到单个对象的确切大小是非常有用的。但是,对象有时候会引用其他对象,而这些其他对象有可能引用另外的对象,以此类推。此时,知道对象的总体大小(包括遍历每个类型域的大小)能够有助于了解一些非常大和复杂的对象。我们可以使用【!objsize】命令。
如果在运行【!ObjSize】命令时没有指定地址,那么这个命令将列出进程中所有托管线程中的所有对象的大小,需要说明一点,在【Windbg Preview】调试器里必须输入一个地址,如果在【NTSD】或者【CDB】调试器中可以执行【!ObjSize】命令,可以不用跟任何参数,效果如图
B、眼见为实
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_8
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.2】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.exe】。
打开【NTSD】调试器窗口。
我们通过【g】命令直接运行调试器,知道调试器输出:名称:PatrickLiu,地址:China-冀州-直隶总督府-广平大街23号-213339。此时,调试器卡住,我们按【ctrl+c】进入调试器中断模式。
我们切换到托管线程0下,执行命令【~0s】。
1 0:001> ~0s 2 ucrtbase!strcmp+0x49: 3 00007ffa`ec654e69 4e8d0c10 lea r9,[rax+r10]
我们使用【!clrstack -a】命令查看一下托管线程的调用栈。
1 0:000> !clrstack -a 2 OS Thread Id: 0x2798 (0) 3 Child SP IP Call Site 4 0000006CF777E238 00007ffaec654e69 [ExternalMethodFrame: 0000006cf777e238] 5 。。。。。。(省略了) 6 0000006CF777E9A0 00007FF996FC1B34 ExampleCore_3_1_8.Program.Main(System.String[]) 7 PARAMETERS: 8 args (0x0000006CF777EA40) = 0x0000023e84c08ea0 9 LOCALS: 10 0x0000006CF777EA28 = 0x0000023e84c09c98 11 0x0000006CF777EA00 = 0x0000000000000000
0x0000023e84c09c98 红色标注的就是 Person 类型的局部变量 person 的地址,我们使用【!dumpobj 0x0000023e84c09c98】查看一下它的详情。
1 0:000> !dumpobj 0x0000023e84c09c98 2 Name: ExampleCore_3_1_8.Person 3 MethodTable: 00007ff997079480 4 EEClass: 00007ff997082008 5 Tracked Type: false 6 Size: 40(0x28) bytes 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff996f31188 4000001 18 System.Int32 1 instance 32 <Age>k__BackingField 11 00007ff996faec08 4000002 8 System.String 0 instance 0000027f16be0500 <Name>k__BackingField 12 00007ff99707d9a0 4000003 10 ...ore_3_1_8.Address 0 instance 0000023e84c09cc0 <HomeAddress>k__BackingField
红色标注就是它的大小。我们再使用【!ObjSize 0x0000023e84c09c98】命令查看一下它的大小。
1 0:000> !ObjSize 0x0000023e84c09c98 2 sizeof(0000023E84C09C98) = 320 (0x140) bytes (ExampleCore_3_1_8.Person)
【!objsize】命令和【!dumpobj】命令输出的大小区别还是挺大的。
【!objsize】命令在【NTSD】调试器模式下,如果没有指定任何地址,它会输出当前进程中所有托管线程上的所有托管对象的大小。Windbg Preview 执行错误:ystem.ArgumentException: Could not parse target object address: 0。
1 0:000> !ObjSize 2 Thread 2798 ([ExternalMethodFrame: 0000006cf777e238] ): 0000006cf777e810 -> 0000023e84c0b148: 4120 (0x1018) bytes (System.Byte[]) 3 Thread 2798 ([ExternalMethodFrame: 0000006cf777e238] ): 0000006cf777e818 -> 0000027f16be0a90: 32 (0x20) bytes (System.String) 4 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): rbp: 0000023e84c0b0b8: 232 (0xe8) bytes (System.Text.DecoderDBCS) 5 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): rsi: 0000023e84c0b148: 4120 (0x1018) bytes (System.Byte[]) 6 Thread 2798 (System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean) [/_/src/libraries/Common/src/System/Text/DBCSDecoder.cs @ 130]): r15: 0000023e84c0c178: 32792 (0x8018) bytes (System.Char[]) 7 Thread 2798 (System.IO.StreamReader.ReadBuffer() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 604]): rbx: 0000023e84c0b058: 37416 (0x9228) bytes (System.IO.StreamReader) 8 Thread 2798 (System.IO.StreamReader.Read() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 338]): rbx: 0000023e84c0b058: 37416 (0x9228) bytes (System.IO.StreamReader) 9 Thread 2798 (System.IO.SyncTextReader.Read() [/_/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @ 53]): rbp+10: 0000006cf777e970 -> 0000023e84c14190: 37440 (0x9240) bytes (System.IO.SyncTextReader) 10 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-58: 0000006cf777e9d8 -> 0000006cf777ea00 (interior): 0 (0x0) bytes (unknown type) 11 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-48: 0000006cf777e9e8 -> 0000006cf777ea00 (interior): 0 (0x0) bytes (unknown type) 12 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-68: 0000006cf777e9c8 -> 0000023e84c0a9d8: 120 (0x78) bytes (System.String) 13 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-60: 0000006cf777e9d0 -> 0000023e84c09cc0: 232 (0xe8) bytes (ExampleCore_3_1_8.Address) 14 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-50: 0000006cf777e9e0 -> 0000027f16be0500: 48 (0x30) bytes (System.String) 15 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-40: 0000006cf777e9f0 -> 0000023e84c09cc0: 232 (0xe8) bytes (ExampleCore_3_1_8.Address) 16 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-38: 0000006cf777e9f8 -> 0000023e84c09c98: 320 (0x140) bytes (ExampleCore_3_1_8.Person) 17 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp-8: 0000006cf777ea28 -> 0000023e84c09c98: 320 (0x140) bytes (ExampleCore_3_1_8.Person) 18 Thread 2798 (ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio 2022\...\ExampleCore_3_1_8\Program.cs @ 13]): rbp+10: 0000006cf777ea40 -> 0000023e84c08ea0: 24 (0x18) bytes (System.String[]) 19 Failed to enumerate GC references. 20 Failed to walk thread 31d4 21 Handle (strong): 0000023E804A1378 -> 0000023E84C0A2D8: 1024 (0x400) bytes (System.Object[]) 22 Handle (strong): 0000023E804A1380 -> 0000023E84C0A228: 176 (0xb0) bytes (System.Object[]) 23 Handle (strong): 0000023E804A1388 -> 0000023E84C0A180: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider) 24 Handle (strong): 0000023E804A1390 -> 0000023E84C0A0D0: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider) 25 Handle (strong): 0000023E804A1398 -> 0000023E82402020: 8232 (0x2028) bytes (System.Object[]) 26 Handle (strong): 0000023E804A13A0 -> 0000023E84C092F0: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider) 27 Handle (strong): 0000023E804A13A8 -> 0000023E84C095B8: 88 (0x58) bytes (System.Diagnostics.Tracing.EventPipeEventProvider) 28 Handle (strong): 0000023E804A13B0 -> 0000023E84C09508: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider) 29 Handle (strong): 0000023E804A13B8 -> 0000023E84C09240: 112 (0x70) bytes (System.Diagnostics.Tracing.EtwEventProvider) 30 Handle (strong): 0000023E804A13C8 -> 0000023E84C00188: 128 (0x80) bytes (System.ExecutionEngineException) 31 Handle (strong): 0000023E804A13D0 -> 0000023E84C00108: 128 (0x80) bytes (System.StackOverflowException) 32 Handle (strong): 0000023E804A13D8 -> 0000023E84C00088: 128 (0x80) bytes (System.OutOfMemoryException) 33 Handle (strong): 0000023E804A13E0 -> 0000023E84C00028: 96 (0x60) bytes (System.Int32[]) 34 Handle (strong): 0000023E804A13E8 -> 0000023E82400028: 87704 (0x15698) bytes (System.Object[]) 35 Handle (pinned): 0000023E804A15F8 -> 0000023E84C00208: 24 (0x18) bytes (System.Object)
2)、使用【Windbg Preview 】调试
调试源码:ExampleCore_3_1_8
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序输出:名称:PatrickLiu,地址:China-冀州-直隶总督府-广平大街23号-213339,我们的调试器也处于卡住的状态。此时点击【break】按钮,就可以调试程序了。
切换到托管线程,执行命令【~0s】。
1 0:007> ~0s 2 ntdll!NtReadFile+0x14: 3 00007ffa`eeaeae54 c3 ret
我们执行命令【!clrstack -a】查看托管线程栈。
1 0:000> !clrstack -a 2 OS Thread Id: 0x3cf8 (0) 3 Child SP IP Call Site 4 000000CD8377E2C0 00007ffaeeaeae54 [InlinedCallFrame: 000000cd8377e2c0] 5 000000CD8377E2C0 00007ffa2d8276eb [InlinedCallFrame: 000000cd8377e2c0] 6 。。。。。。(省略了) 7 8 000000CD8377E590 00007ff997991b34 ExampleCore_3_1_8.Program.Main(System.String[]) [E:\Visual Studio\...\ExampleCore_3_1_8\Program.cs @ 13] 9 PARAMETERS: 10 args (0x000000CD8377E630) = 0x0000024827408ea0 11 LOCALS: 12 0x000000CD8377E618 = 0x0000024827409c98 13 0x000000CD8377E5F0 = 0x0000000000000000
0x0000024827409c98 红色标注的就是 Person 类型的局部变量 person 的地址。我们可以使用【!dumpobj 0x0000024827409c98】命令查看这个对象的大小。
1 0:000> !dumpobj 0x0000024827409c98 2 Name: ExampleCore_3_1_8.Person 3 MethodTable: 00007ff997a49480 4 EEClass: 00007ff997a52008 5 Tracked Type: false 6 Size: 40(0x28) bytes(这里就是大小) 7 File: E:\Visual Studio 2022\...\ExampleCore_3_1_8\bin\Debug\net8.0\ExampleCore_3_1_8.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007ff997901188 4000001 18 System.Int32 1 instance 32 <Age>k__BackingField 11 00007ff99797ec08 4000002 8 System.String 0 instance 00000288b93b0500 <Name>k__BackingField 12 00007ff997a4d9a0 4000003 10 ...ore_3_1_8.Address 0 instance 0000024827409cc0 <HomeAddress>k__BackingField
我们使用【!objsize 0x0000024827409c98】这个命令看看它的大小。
1 0:000> !objsize 0x0000024827409c98 2 Objects which 24827409c98(ExampleCore_3_1_8.Person) transitively keep alive: 3 4 Address MT Size 5 024827409c98 7ff997a49480 40 6 0288b93b0500 7ff99797ec08 42 7 024827409cc0 7ff997a4d9a0 56 8 0288b93b0530 7ff99797ec08 32 9 0288b93b0550 7ff99797ec08 26 10 0288b93b0570 7ff99797ec08 32 11 0288b93b0590 7ff99797ec08 36 12 0288b93b05b8 7ff99797ec08 34 13 14 Statistics: 15 MT Count TotalSize Class Name 16 7ff997a49480 1 40 ExampleCore_3_1_8.Person 17 7ff997a4d9a0 1 56 ExampleCore_3_1_8.Address 18 7ff99797ec08 6 202 System.String 19 Total 8 objects, 298 bytes
这个结果还是很详细的。我们看到了【!objsize】命令得到的是 298 bytes,而【!dumpobj】得到的大小是 40 bytes,这个差别还是挺大的。
A、基础知识
Windows 的异常模型采用的是结构化异常处理(Structured Exception Handling,SEH)。同样,.NET异常模型也是构建在 Windows SEH模型之上的,并提供了基于对象的异常模型。CLR 在每个异常内携带的额外信息都被保存到托管堆上。所有 CLR 异常都以 SEH 异常的形式出现,错误码是:0xe0434352。既然所有的异常的错误码都是 0xe0434352,我们是如何区分异常的不同的呢?答案就是依靠异常中保存的扩展信息。(说明,原书错误码是:0xe0434f4d,平台是 .NET Framework,我的错误码是:0xe0434352,平台是.NET 8.0),
使用 Windbg Preview 调试器的效果如图:
使用 NTSD 调试器的效果如图:
原著中的异常代码如图:
在 CLR 看来,异常也是一种引用类型,所以也可以使用【!dumpobj】命令输出它的信息。这个命令有些繁琐,如果我们只希望显示异常的类型、栈回溯(包括内部栈回溯)以及信息,我们就可以使用【!PrintException】命令。这个命令的参数托管异常的地址。当然,我们在分析异常时,【!threads】命令也会经常用得到,它能显示出系统中各个托管线程的信息,包括该线程抛出的最后一个异常。
【StopOnException】命令是在抛出特定异常时设置一个断点,和异常信息的转储关系不大。
命令的格式:
!StopOnException [-derived] [-create|-create2] <Exception> [<Pseudo-register number>],create 和 create2 这两个开关控制着断点是在第一次出现指定的异常时触发,还是第二次出现时触发。derived 开关会增加断点的范围,不仅包含指定的异常,而且还包括从指定异常派生的任意异常。pseudo-register 是可选的,表示命令将使用哪个伪寄存器设置断点,如果没有指定伪寄存器,默认是 $t1。
例子:
!StopOnException -create System.ArgumentException,表示当第一次抛出 System.ArgumentException 异常时设置断点。
!StopOnException -derived System.Exception ,表示对从 System.Exception 派生下来的任意异常都设置一个断点。
B、眼见为实
1)、使用【NTSD】调试
调试源码:ExampleCore_3_1_6
编译项目,打开【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】调试器。
使用【g】命令运行调试器,直到调试器抛出异常,调试器中断执行。
我们使用【kb】命令,查看托管调用栈的栈回溯。
0:000> kb RetAddr : Args to Child : Call Site 00007ffb`00a9ca6f : 000001b8`72c0aca8 0000002c`9517e710 0000002c`9517ea00 00007ffb`00a7bbb4 : KERNELBASE!RaiseException+0x69 00007ffb`00a9c129 : 00000000`70000185 00007ffb`00d961c8 0000002c`9517eb88 0000002c`9517ed28 : coreclr!RaiseTheExceptionInternalOnly+0x26b 00007ffa`a0fc2416 : 000001b8`72c0aca8 000001f9`04c913c8 000001b8`700fa1e8 00000000`00000040 : coreclr!IL_Throw+0xb9 00007ffa`a0fc1afd : 000001b8`72c09ce0 00000000`00000000 000001b8`6e7af410 000001b8`72c09c38 : 0x00007ffa`a0fc2416 00007ffb`00b2a1a3 : 000001b8`72c08ea0 0000002c`9517f128 0000002c`9517f128 0000002c`9517ed19 : 0x00007ffa`a0fc1afd 00007ffb`00ab14c9 : 00000000`00000000 00000000`00000130 0000002c`9517ed28 00007ffb`009da456 : coreclr!CallDescrWorkerInternal+0x83 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 00007ffb`009d75ac : 0000002c`9517eda8 00000000`00000000 00000000`00000048 00007ffb`00ac28a6 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb 00007ffb`009d6f7a : 000001b8`72c08ea0 000001b8`72c08ea0 00000000`00000000 0000002c`9517f128 : coreclr!RunMainInternal+0x11c 00007ffb`009d6b17 : 000001b8`6e7af410 000001b8`00000000 000001b8`6e7af410 00000000`00000000 : coreclr!RunMain+0xd2 00007ffb`009d7321 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf 00007ffb`00ae7768 : 00000000`00000001 0000002c`9517f301 0000002c`9517f350 00007ffb`86ff23ea : coreclr!CorHost2::ExecuteAssembly+0x281 00007ffb`87012c36 : 000001b8`6e780fd0 000001b8`6e780310 00000000`00000000 000001b8`6e780310 : coreclr!coreclr_execute_assembly+0xd8 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a 00007ffb`87012f1c : 000001b8`6e76bcc8 0000002c`9517f579 00007ffb`8704c9c0 000001b8`6e76bcc8 : hostpolicy!run_app_for_context+0x596 00007ffb`8701385a : 00000000`00000000 000001b8`6e76bcc0 000001b8`6e76bcc0 00000000`00000000 : hostpolicy!run_app+0x3c 00007ffb`b3e2b5c9 : 000001b8`6e77f438 000001b8`6e77f320 00000000`00000000 0000002c`9517f679 : hostpolicy!corehost_main+0x15a 00007ffb`b3e2e066 : 000001b8`6e77e330 0000002c`9517fa00 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 00007ffb`b3e302ec : 00007ffb`b3e625f8 000001b8`6e77cb40 0000002c`9517f940 0000002c`9517f8f0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 00007ffb`b3e2e644 : 0000002c`9517fa00 0000002c`9517fa20 0000002c`9517f971 000001b8`6e77cf60 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c 00007ffb`b3e285a0 : 0000002c`9517fa20 000001b8`6e77c440 00000000`00000001 000001b8`6e760000 : hostfxr!fx_muxer_t::execute+0x494 *** WARNING: Unable to verify checksum for apphost.exe 00007ff6`5faef998 : 00007ffb`d241f4e8 00007ffb`b3e29b10 0000002c`9517fbc0 000001b8`6e77c130 : hostfxr!hostfxr_main_startupinfo+0xa0 00007ff6`5faefda6 : 00007ff6`5fafb6c0 00000000`00000007 000001b8`6e76bcc0 00000000`0000005e : apphost!exe_start+0x878 00007ff6`5faf12e8 : 00000000`00000000 00000000`00000000 000001b8`6e76bcc0 00000000`00000000 : apphost!wmain+0x146 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22 00007ffb`d33f6fd4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c 00007ffb`d4b7cec1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21 0:000>
接下来,我们找到 KERNELBASE!RaiseException 函数(000001b8`72c0aca8)的第一个参数,在该地址上执行【!dumpobj】命令,查看异常详情。
1 0:000> !DumpObj 000001b8`72c0aca8 2 Name: System.ArgumentException 3 MethodTable: 00007ffaa10467b0 4 EEClass: 00007ffaa0f8cdf8 5 Tracked Type: false 6 Size: 136(0x88) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 0000000000000000 4000264 8 ...ection.MethodBase 0 instance 0000000000000000 _exceptionMethod 11 00007ffaa0faec08 4000265 10 System.String 0 instance 000001f904c913c8 _message 12 00007ffaa1043060 4000266 18 ...tions.IDictionary 0 instance 0000000000000000 _data 13 00007ffaa1040820 4000267 20 System.Exception 0 instance 0000000000000000 _innerException 14 00007ffaa0faec08 4000268 28 System.String 0 instance 0000000000000000 _helpURL 15 00007ffaa10aa858 4000269 30 System.Byte[] 0 instance 000001b872c0ad78 _stackTrace 16 00007ffaa10aa858 400026a 38 System.Byte[] 0 instance 0000000000000000 _watsonBuckets 17 00007ffaa0faec08 400026b 40 System.String 0 instance 0000000000000000 _stackTraceString 18 00007ffaa0faec08 400026c 48 System.String 0 instance 0000000000000000 _remoteStackTraceString 19 00007ffaa0efc4d8 400026d 50 System.Object[] 0 instance 0000000000000000 _dynamicMethods 20 00007ffaa0faec08 400026e 58 System.String 0 instance 0000000000000000 _source 21 00007ffaa0fa8b78 400026f 60 System.UIntPtr 1 instance 00007FFAA0FC2415 _ipForWatsonBuckets 22 00007ffaa0fa70a0 4000270 68 System.IntPtr 1 instance 0000000000000000 _xptrs 23 00007ffaa0f31188 4000271 70 System.Int32 1 instance -532462766 _xcode 24 00007ffaa0f31188 4000272 74 System.Int32 1 instance -2147024809 _HResult 25 00007ffaa0faec08 4000383 78 System.String 0 instance 0000000000000000 _paramName 26 0:000>
以上就是异常的详情,如果我们想查看具体提示消息,可以针对 _message 域的地址 000001f904c913c8 再次执行【!DumpObj】命令。
1 0:000> !dumpobj 000001f904c913c8 2 Name: System.String 3 MethodTable: 00007ffaa0faec08 4 EEClass: 00007ffaa0f8a500 5 Tracked Type: false 6 Size: 58(0x3a) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: Obj cannot be null 9 Fields: 10 MT Field Offset Type VT Attr Value Name 11 00007ffaa0f31188 400033b 8 System.Int32 1 instance 18 _stringLength 12 00007ffaa0f3b538 400033c c System.Char 1 instance 4f _firstChar 13 00007ffaa0faec08 400033a c8 System.String 0 static 000001f904c90008 Empty
【!dumpobj】命令有些繁琐,我们可以直接使用【!PrintException】命令输出异常信息。
1 0:000> !PrintException 000001b8`72c0aca8 2 WARNING: SOS needs to be upgraded for this version of the runtime. Some commands may not work correctly. 3 For more information see https://go.microsoft.com/fwlink/?linkid=2135652 4 5 Exception object: 000001b872c0aca8 6 Exception type: System.ArgumentException 7 Message: Obj cannot be null 8 InnerException: <none> 9 StackTrace (generated): 10 SP IP Function 11 0000002C9517E9C0 00007FFAA0FC2415 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException(ExampleCore_3_1_6.ObjTypes)+0x85 12 0000002C9517EA10 00007FFAA0FC1AFC ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main(System.String[])+0x1cc 13 14 StackTraceString: <none> 15 HResult: 80070057
在这里看的很清楚,包括异常的类型(Exception type:System.ArgumentException),异常的地址(Exception object: 000001b872c0aca8),异常抛出的方法(ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException)等。
当然,我们可以使用【!threads】或者【!t】命令输出所有托管线程的信息,包括托管线程抛出的最有一个异常。
【!threads】命令执行的效果。
1 0:000> !threads 2 ThreadCount: 3 3 UnstartedThread: 0 4 BackgroundThread: 2 5 PendingThread: 0 6 DeadThread: 0 7 Hosted Runtime: no 8 Lock 9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 10 0 1 1190 000001B86E7AF410 2a020 Preemptive 000001B872C0ADF0:000001B872C0C638 000001B86E7F1530 -00001 MTA System.ArgumentException 000001b872c0aca8 11 6 2 3b64 000001B8700EDDA0 21220 Preemptive 0000000000000000:0000000000000000 000001B86E7F1530 -00001 Ukn (Finalizer) 12 7 3 1630 000001B86E7BBF00 2b220 Preemptive 0000000000000000:0000000000000000 000001B86E7F1530 -00001 MTA
红色标注的就是 ID 为 1 的托管线程抛出的异常(System.ArgumentException),异常的地址:000001b872c0aca8 。
2)、使用【Windbg Preview】调试
调试源码:ExampleCore_3_1_6
编译项目,打开 Windbg,点击【文件】----》【launch executable】附加程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,直到我们的控制台程序抛出一个异常,调试器停止执行。
1 0:000> g 2 ModLoad: 00007ffb`d4ac0000 00007ffb`d4af0000 C:\Windows\System32\IMM32.DLL 3 ModLoad: 00007ffb`c8d40000 00007ffb`c8d99000 C:\Program Files\dotnet\host\fxr\8.0.2\hostfxr.dll 4 ModLoad: 00007ffb`c8cd0000 00007ffb`c8d34000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\hostpolicy.dll 5 ModLoad: 00007ffa`fcab0000 00007ffa`fcf98000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\coreclr.dll 6 ModLoad: 00007ffb`d3b60000 00007ffb`d3c89000 C:\Windows\System32\ole32.dll 7 ModLoad: 00007ffb`d34a0000 00007ffb`d37f4000 C:\Windows\System32\combase.dll 8 ModLoad: 00007ffb`d3200000 00007ffb`d32d5000 C:\Windows\System32\OLEAUT32.dll 9 ModLoad: 00007ffb`d1fc0000 00007ffb`d203f000 C:\Windows\System32\bcryptPrimitives.dll 10 (2e10.1988): Unknown exception - code 04242420 (first chance) 11 ModLoad: 00007ffa`fb810000 00007ffa`fc49c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 12 ModLoad: 00007ffa`fb650000 00007ffa`fb809000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll 13 ModLoad: 00007ffb`d1f00000 00007ffb`d1f13000 C:\Windows\System32\kernel.appcore.dll 14 ModLoad: 00000293`f6f10000 00000293`f6f18000 E:\Visual Studio 2022\...\ExampleCore_3_1_6\bin\Debug\net8.0\ExampleCore_3_1_6.dll 15 ModLoad: 00000293`f6f30000 00000293`f6f3e000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll 16 ModLoad: 00007ffb`c8c70000 00007ffb`c8c98000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll 17 ModLoad: 00007ffb`79470000 00007ffb`79482000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Threading.dll 18 ModLoad: 00000293`f6f40000 00000293`f6f48000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Text.Encoding.Extensions.dll 19 ModLoad: 00007ffb`5dc90000 00007ffb`5dca5000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.InteropServices.dll 20 (2e10.1988): CLR exception - code e0434352 (first chance) 21 (2e10.1988): CLR exception - code e0434352 (!!! second chance !!!) 22 KERNELBASE!RaiseException+0x69: 23 00007ffb`d2563e49 0f1f440000 nop dword ptr [rax+rax]
我们使用【kb】命令查看非托管线程的调用栈,并显示前4个参数。
1 0:000> kb 2 # RetAddr : Args to Child : Call Site 3 00 00007ffa`fcb7ca6f : 00000293`fb40aca8 000000d7`0477e1f0 000000d7`0477e4e0 00007ffa`fcb5bbb4 : KERNELBASE!RaiseException+0x69 4 01 00007ffa`fcb7c129 : 00000000`70000185 00007ffa`fce761c8 000000d7`0477e668 000000d7`0477e808 : coreclr!RaiseTheExceptionInternalOnly+0x26b [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 2795] 5 02 00007ffa`9d0a24b6 : 00000293`fb40aca8 000002d4`8d5813c8 00000293`f6fbe1e8 00000000`00000040 : coreclr!IL_Throw+0xb9 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 4247] 6 03 00007ffa`9d0a1afd : 00000293`fb409ce0 00000000`00000000 00000293`f6fa6280 00000293`fb409c38 : ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException+0x86 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\Program.cs @ 76] 7 04 00007ffa`fcc0a1a3 : 00000293`fb408ea0 000000d7`0477ec08 000000d7`0477ec08 000000d7`0477e7f9 : ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main+0x1cd [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_6\Program.cs @ 46] 8 05 00007ffa`fcb914c9 : 00000000`00000000 00000000`00000130 000000d7`0477e808 00007ffa`fcaba456 : coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100] 9 06 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67] 10 07 00007ffa`fcab75ac : 000000d7`0477e888 00000000`00000000 00000000`00000048 00007ffa`fcba28a6 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 570] 11 08 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb [D:\a\_work\1\s\src\coreclr\vm\callhelpers.h @ 458] 12 09 00007ffa`fcab6f7a : 00000293`fb408ea0 00000293`fb408ea0 00000000`00000000 000000d7`0477ec08 : coreclr!RunMainInternal+0x11c [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1304] 13 0a 00007ffa`fcab6b17 : 00000293`f6fa6280 00000293`00000000 00000293`f6fa6280 00000000`00000000 : coreclr!RunMain+0xd2 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1375] 14 0b 00007ffa`fcab7321 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1504] 15 0c 00007ffa`fcbc7768 : 00000000`00000001 000000d7`0477ee01 000000d7`0477ee30 00007ffb`c8cd23ea : coreclr!CorHost2::ExecuteAssembly+0x281 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349] 16 0d 00007ffb`c8cf2c36 : 00000293`f6f7a900 00000293`f6f7a660 00000000`00000000 00000293`f6f7a660 : coreclr!coreclr_execute_assembly+0xd8 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 504] 17 0e (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 109] 18 0f 00007ffb`c8cf2f1c : 00000293`f6f67da8 000000d7`0477f059 00007ffb`c8d2c9c0 00000293`f6f67da8 : hostpolicy!run_app_for_context+0x596 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256] 19 10 00007ffb`c8cf385a : 00000000`00000000 00000293`f6f67da0 00000293`f6f67da0 00000000`00000000 : hostpolicy!run_app+0x3c [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285] 20 11 00007ffb`c8d4b5c9 : 00000293`f6f769e8 00000293`f6f768d0 00000000`00000000 000000d7`0477f159 : hostpolicy!corehost_main+0x15a [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426] 21 12 00007ffb`c8d4e066 : 00000293`f6f77540 000000d7`0477f4e0 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145] 22 13 00007ffb`c8d502ec : 00007ffb`c8d825f8 00000293`f6f780d0 000000d7`0477f420 000000d7`0477f3d0 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532] 23 14 00007ffb`c8d4e644 : 000000d7`0477f4e0 000000d7`0477f500 000000d7`0477f451 00000293`f6f78401 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007] 24 15 00007ffb`c8d485a0 : 000000d7`0477f500 00000293`f6f76690 00000000`00000001 00000293`f6f60000 : hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578] 25 16 00007ff7`6c9ff998 : 00007ffb`d241f4e8 00007ffb`c8d49b10 000000d7`0477f6a0 00000293`f6f76380 : hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62] 26 17 00007ff7`6c9ffda6 : 00007ff7`6ca0b6c0 00000000`00000007 00000293`f6f67da0 00000000`0000005e : apphost!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240] 27 18 00007ff7`6ca012e8 : 00000000`00000000 00000000`00000000 00000293`f6f67da0 00000000`00000000 : apphost!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311] 28 19 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : apphost!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90] 29 1a 00007ffb`d33f6fd4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : apphost!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 30 1b 00007ffb`d4b7cec1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14 31 1c 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
接下来,找出 KERNELBASE!RaiseException 函数的(000001f8`4c80aca8)第一个参数,并在这个地址上执行【!dumpobj 000001f8`4c80aca8】命令。
1 0:000> !DumpObj 000001f8`4c80aca8 2 Name: System.ArgumentException 3 MethodTable: 00007ffaa06e67b0 4 EEClass: 00007ffaa062cdf8 5 Tracked Type: false 6 Size: 136(0x88) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 0000000000000000 4000264 8 ...ection.MethodBase 0 instance 0000000000000000 _exceptionMethod 11 00007ffaa064ec08 4000265 10 System.String 0 instance 00000238deaf13c8 _message 12 00007ffaa06e3060 4000266 18 ...tions.IDictionary 0 instance 0000000000000000 _data 13 00007ffaa06e0820 4000267 20 System.Exception 0 instance 0000000000000000 _innerException 14 00007ffaa064ec08 4000268 28 System.String 0 instance 0000000000000000 _helpURL 15 00007ffaa074a858 4000269 30 System.Byte[] 0 instance 000001f84c80ad78 _stackTrace 16 00007ffaa074a858 400026a 38 System.Byte[] 0 instance 0000000000000000 _watsonBuckets 17 00007ffaa064ec08 400026b 40 System.String 0 instance 0000000000000000 _stackTraceString 18 00007ffaa064ec08 400026c 48 System.String 0 instance 0000000000000000 _remoteStackTraceString 19 00007ffaa059c4d8 400026d 50 System.Object[] 0 instance 0000000000000000 _dynamicMethods 20 00007ffaa064ec08 400026e 58 System.String 0 instance 0000000000000000 _source 21 00007ffaa0648b78 400026f 60 System.UIntPtr 1 instance 00007FFAA0662415 _ipForWatsonBuckets 22 00007ffaa06470a0 4000270 68 System.IntPtr 1 instance 0000000000000000 _xptrs 23 00007ffaa05d1188 4000271 70 System.Int32 1 instance -532462766 _xcode 24 00007ffaa05d1188 4000272 74 System.Int32 1 instance -2147024809 _HResult 25 00007ffaa064ec08 4000383 78 System.String 0 instance 0000000000000000 _paramName
从【!dumpobj】命令的输出中,我们可以看到托管代码异常的所有信息,包括异常的类型(System.ArgumentException)以及异常相关的所有域。我们可以针对标红的 _message 再次执行【!dumpobj 00000238deaf13c8】命令,查看异常的具体提示信息。
1 0:000> !dumpobj 00000238deaf13c8 2 Name: System.String 3 MethodTable: 00007ffaa064ec08 4 EEClass: 00007ffaa062a500 5 Tracked Type: false 6 Size: 58(0x3a) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: Obj cannot be null 9 Fields: 10 MT Field Offset Type VT Attr Value Name 11 00007ffaa05d1188 400033b 8 System.Int32 1 instance 18 _stringLength 12 00007ffaa05db538 400033c c System.Char 1 instance 4f _firstChar 13 00007ffaa064ec08 400033a c8 System.String 0 static 00000238deaf0008 Empty
简写方式输出。
1 0:000> !DumpObj -nofields 00000238deaf13c8 2 Name: System.String 3 MethodTable: 00007ffaa064ec08 4 EEClass: 00007ffaa062a500 5 Tracked Type: false 6 Size: 58(0x3a) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll 8 String: Obj cannot be null
上面我们使用【!dumpobj】命令可以查看异常信息,但是比较繁琐。我们可以使用【!PrintException】命令输出异常信息。
1 0:000> !PrintException 000001f8`4c80aca8 2 Exception object: 000001f84c80aca8 3 Exception type: System.ArgumentException 4 Message: Obj cannot be null 5 InnerException: <none> 6 StackTrace (generated): 7 SP IP Function 8 0000007BA577E6D0 00007FFAA0662415 ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.ThrowException(ExampleCore_3_1_6.ObjTypes)+0x85 9 0000007BA577E720 00007FFAA0661AFC ExampleCore_3_1_6!ExampleCore_3_1_6.ObjTypes.Main(System.String[])+0x1cc 10 11 StackTraceString: <none> 12 HResult: 80070057
这里输出的内容更清晰,是哪个方法抛出的异常都可以看到(ThrowException)。我们分析异常的时候,可以使用【!threads】或者【!t】命令查看所有托管线程信息,包括该线程抛出的最后一个异常。
1 0:000> !threads 2 ThreadCount: 3 3 UnstartedThread: 0 4 BackgroundThread: 2 5 PendingThread: 0 6 DeadThread: 0 7 Hosted Runtime: no 8 Lock 9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 10 0 1 379c 000001F8485263D0 2a020 Preemptive 000001F84C80ADF0:000001F84C80C638 000001f848568920 -00001 MTA System.ArgumentException 000001f84c80aca8 11 6 2 10d4 000001F8485D7C40 21220 Preemptive 0000000000000000:0000000000000000 000001f848568920 -00001 Ukn (Finalizer) 12 7 3 3204 000001F8485346F0 2b220 Preemptive 0000000000000000:0000000000000000 000001f848568920 -00001 MTA 13 14 15 0:000> !t 16 ThreadCount: 3 17 UnstartedThread: 0 18 BackgroundThread: 2 19 PendingThread: 0 20 DeadThread: 0 21 Hosted Runtime: no 22 Lock 23 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 24 0 1 379c 000001F8485263D0 2a020 Preemptive 000001F84C80ADF0:000001F84C80C638 000001f848568920 -00001 MTA System.ArgumentException 000001f84c80aca8 25 6 2 10d4 000001F8485D7C40 21220 Preemptive 0000000000000000:0000000000000000 000001f848568920 -00001 Ukn (Finalizer) 26 7 3 3204 000001F8485346F0 2b220 Preemptive 0000000000000000:0000000000000000 000001f848568920 -00001 MTA
我们看到了 ID 为 1 的线程抛出一个类型为 System.ArgumentException 异常,异常的地址:000001f84c80aca8 ,我们可以针对这个地址使用【!DumpObj】或者【!PrintException】命令查看异常的详情。
五、总结
这篇文章的第二部分终于写完了,考虑到内容太多,分多篇写作了。只有所有代码自己都写了,都测试了,才会明白里面发生的事情。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。