一、介绍
这是我的《Advanced .Net Debugging》这个系列的第十篇文章。这篇文章的内容是原书的第三部分的【高级主题】的第八章【事后调试】。前面几篇文章,我们介绍了很多工具,可以帮助大家找出问题的所在。但是,有一类问题我们是没办法使用这些工具来解决的,那就是已经发布的程序。在程序发布后,总是会出现一些问题,并且这些问题出现的时机是不确定的,大多数出现在用户在使用软件的过程中。想要解决这样的问题,我们当然会想到远程调试,有时候可以,有时候是不可以的,当然,不可以的理由会有很多,比如:安全的原因等类似的。
如果使用软件的客户拒绝对出现问题的软件进行实时调试,并且本地又无法重现问题,那我们该怎么办呢?当然是有办法的,那就是【事后调试】。
【事后调试】的步骤:
1)、触发故障的发生。
2)、抓取系统在发生故障时的状态快照(根据不同的故障类型,在某些情况下还需要抓取故障发生前后的状态快照)。
3)、将快照发送给工程师作进一步的分析。
在这篇文章中,我们将讨论抓取快照的各种不同方式,不同类型的转储信息以及如何分析它们。
当然,高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。
如果在没有说明的情况下,所有代码的测试环境都是 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源码:源码下载
在此说明:我使用了两种调试工具,第一种:Windbg Preivew,图形界面,使用方便,操作顺手,不用担心干扰;第二种是:NTSD,是命令行形式的调试器,在命令使用上和 Windbg 没有任何区别,之所以增加了它的调试过程,不过是我的个人爱好,想多了解一些,看看他们有什么区别,为了学习而使用的。如果在工作中,我推荐使用 Windbg Preview,更好用,更方便,也不会出现奇怪问题(我在使用 NTSD 调试断点的时候,不能断住,提示内存不可读,Windbg preview 就没有任何问题)。
如果大家想了解调试过程,二选一即可,当然,我推荐查看【Windbg Preview 调试】。
二、目录结构
为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。
2.1、转储文件基本知识
2.1.1、通过调试器来生成转储文件
A、基础知识
B、眼见为实
1)、NTSD 调试
2)、Windbg Preview 调试
2.1.2、通过 ADPlus 生成转储文件
A、基础知识
B、眼见为实
1)、崩溃模式
2.1.3、转储文件的调试
2.1.4、数据访问层
2.1.5、转储文件分析:未处理的 .NET 异常
1)、NTSD 调试
2)、Windbg Preview 调试
2.2、Windows 错误报告
三、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
1、ExampleCore_8_01
1 namespace ExampleCore_8_01 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Program program = new Program(); 8 program.Run(); 9 } 10 11 public void Run() 12 { 13 Console.WriteLine("Press any key to start"); 14 Console.ReadKey(); 15 ProcessData(null); 16 } 17 18 public void ProcessData(string? data) 19 { 20 if (data == null) 21 { 22 throw new ArgumentNullException("Argument is Null"); 23 } 24 string s = "Hello:" + data; 25 } 26 } 27 }
四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。
4.1、转储文件的基本知识
转储文件是进程状态的外在表示。生成转储文件的目的:在不需要对出问题的计算机进行实时访问的情况下,就可以对程序故障进行分析。有了转储文件,调试人员就可以使用调试器的事后调试功能来分析故障。
共有两种类型的转储文件:
1)、完全转储文件(Full Dump)。
2)、微型转储文件(Mini Dump)。
在完全转储文件中包含了进程的整个内存空间、可执行映像、句柄表和调试器需要使用的其他信息。当使用完全转储文件时不能指定所要收集的数据量。但是,我们可以使用调试器将完全转储文件转换为微型转储文件。
微型转储文件的内容是可变的,并且能根据使用的转储文件生成器进行定制。在微型转储文件中可以只包含某个线程的信息,也可以包含被转储进程的详细信息。需要注意的是,在最大的微型转储文件中包含的内容要多于在完全转储文件中包含的内容。因此,我们这里只介绍微型转储文件的结构。
以下是能够生成转储文件的工具列表:
1)、Windows Debuggers(Windows 调试器):Windows 调试器可以生成各种不同大小的转储文件,并且能够完全控制转储文件的生成过程。
2)、ADPlus:ADPlus 是 Windows 调试工具集中的一个。它的作用相当于一个进程监视器,每当发生崩溃或者挂起时,都能生成转储文件。此外,它还能将崩溃事件通知给用户。
3)、Windows 错误报告:Windows错误报告是Microsoft提供的一种服务,用户通过这种服务注册到一个实时的错误报告站点。每当用户的某个应用程序发生错误时,都会将一个错误报告从发生崩溃的机器发送到Windows错误报告站点。然后,在进行事后分析时可以从WER服务中提取崩溃信息(包括转储文件)。
以上都是书里介绍的内容,由于书写的比较早,到现在还有很多其他工具可以生成转储文件,比如:任务管理器,Windbg 调试器,Process Explorer,PCHunter 等,使用起来也很方便,网上学习资料很多,我就不多说了。
接下来,我们就针对这三种工具分别介绍如何生成转储文件。
4.1.1、通过调试器生成转储文件
A、基础知识
如果我们想使用调试器生成 DUMP 文件,可以使用【.dump】命令,【.dump /m】表示调试器将生成一个微型转储文件。当然【.dump】命令还有其他的选项,如下:
a、生成一个完整的微型转储文件,启动所有选项。在这个文件中将包含完整的内存数据、句柄信息、模块信息、基本的内存信息和线程信息等。相当于使用/mfFhut。
f、生成一个微型转储文件,其中包含进程内所有可访问和已提交的内存页。
F、生成一个微型转储文件,其中包含调试器在重构整个虚拟内存地址空间时需要的所有基本内存信息。
h、生成一个微型转储文件,其中包含句柄信息。
u、生成一个微型转储文件,其中包含未卸载模块的信息。注意,这个选项只能在Windows Server 2003上使用。
t、生成一个微型转储文件,其中包含线程时间的信息。在线程时间信息中包括创建时间,以及在用户态和内核态中执行的时间。
i、生成一个微型转储文件,其中包含辅助内存信息。辅助内存是指由栈指针或者后台存储使用的内存(及其周围的一小块内存)。
p、生成一个微型转储文件,其中包含进程环境块和线程环境块。
w、生成一个微型转储文件,其中包含所有已提交的读-写私有内存页。
d、生成一个微型转储文件,其中包含映像中的所有数据段。
c、生成一个微型转储文件,其中包含映像中的所有代码段。
r、生成一个微型转储文件,适合于在需要保护隐私的情况中使用。这个选项将删除在重建栈时不需要的任何信息(将这些信息替换为0,包括局部变量)。
R、生成一个微型转储文件,适合在需要保护隐私的情况下使用。这个选项将从微型转储文件中删除完整的模块路径,因此将确保用户目录结构的隐私性。
举个例子:我们在调试器中执行【.dump /ma /u F:\MyDump.dmp】命令,可以在F盘看到生成 dmp 文件到MyDump_1c90_2024-06-27_15-13-24-466_3660.dmp文件。 .dump命令参数比较多,常用的组合就是/ma,/m 表示生成minidump,/a 表示dmp包含所有信息,/u 参数就是上面说的附加时间和PID信息到文件名。
1 0:000> .dump /ma /u F:\Test\TestDump\MyDump.dmp 2 Creating F:\Test\TestDump\MyDump_1c90_2024-06-27_15-13-24-466_3660.dmp - mini user dump 3 Dump successfully written
当我们在生成一个转储文件的时候,有一个经验法则,在转储文件中包含的状态越多,在进行事后调试的时候就能获取更多的信息。当然,最大的限制因素就是转储文件的大小。有时候你会发现无法从一个高安全的服务器上获取很大的转储文件,而只能对一个删除了某些敏感信息之后的转储文件进行分析。
如果我们使用命令行调试器,当我们需要加载 dump 文件时,必须使用【ntsd -z dumpFileName】命令才可以,我使用的是【ntsd】调试器。dumpFileName:必须包含 dump 文件的完整路径和后缀名。
通过调试器生成转储文件的一个难点就是调试器必须在合适的时候被附加到故障进程上。一般来说,还好,但是对于崩溃情况是不定时的,或者是没有规律的,这样就很容易错失附加调试器的机会。当我们的程序发生崩溃的时候
Windows 可以自动的生成转储文件就好了,这种机制是有的。我们可以使用“事后调试器设置(Postmortem Debugger Setup)”。我们可以使用以下命令来修改事后调试器:windbg -I、cdb -iae、ntsd -iae、drwtsn32 -i
效果如图:
想要执行以上命令,直接打开【cmd】命令行工具,输入命令【windbg -I】、【cdb -iae】、【ntsd -iae】、【drwtsn32 -i】就可以了。
修改注册表的值:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
效果如图:
这个结果是我执行【cdb -iae】命令的结果。命令窗口如图:
我们可以直接运行我们的实例程序,控制台程序一旦抛出异常,立刻就能打开我们注册调试器。
1)先演示执行【Windbg -I】命令后的效果。
双击我们的控制台程序,看到“Press any key to start”字样,然后点击回车键,就会打开指定的调试器。
效果如图:
2)、先演示执行【cdb -iae】命令后的效果。
双击我们的控制台程序,看到“Press any key to start”字样,然后点击回车键,就会打开指定的调试器。
效果如图:
转储文件生成注意事项:
在
Windows Vista 中修改了错误报告技术在本地机器上保存转储文件的方式。在 Windows Vista 之前,Dr.Watson
默认将生成的转储文件保存在本地机器上。这些转储文件可以由任何一个想要调试转储文件的用户访问。在 Windows Vista 中去掉了
Dr.Watson,而是引入了一种更为可靠和稳定的错误报告机制。其中的修改之一就是,生成的转储文件(在默认情况下)不会被保存到本地机器上。要改变这种默认行为,可以将注册表
ForceQueue 设置为1,这将使所有转储文件在上传到 Microsoft 之前就在本地机器上排队。ForceQueue
注册键值位于以下注册路径:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error
Reporting
在注册键值 ForceQueue 被设置为1后,生成的所有转储文件都将被保存到以下位置:
对于在系统上下文中运行或者被提升到系统上下文中运行的进程:
%ALLUSERSPROFILE %\Microsoft\Windows\WER\[ReportQueue|ReportArchive]
对于所有其他的进程:
%LOCALAPPDATA%\Microsoft\Windows\WER\[ReportQueue|ReportArchive]
当调试非托管代码程序时,会用到 AeDebug
键值。然而,对于托管代码调试,可以使用【DbgManagedDebugger】和【DbgJITDebugLaunchSetting】这两个键值控制器调试。该键值位置:计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework,效果如图:
1)、DbgManagedDebugger:该注册键值会指定当遇到未处理的异常时应该启动哪一个调试器。当遇到未处理的异常时,该注册键值指定的调试器并不会立即调用,而是显示一个消息框,由用户选择是调试程序还是结束程序。在事后调试中,一个很常见的问题就是,如何在程序发生故障的时候自动生成转储文件。
我们可以执行以下命令【ntsd
-pv -p %ld -c ",dump /u /ma <path to dump file>; .kill;
qd】,该命令的意思是,当一个故障发生时,启动【ntsd】调试器,并且执行【.dump】命令生成一个转储文件,然后退出调试回话。
2)、DbgJITDebugLaunchSetting:该注册键值表示发生未处理异常时的行为。如果这个值被设置为 0,那么将显示一个消息框,并由用户选择对故障采用何种处理方式。请注意,只有在交互进程的情况下才会显示这个消息框,而对于其他进程(例如服务)则是直接结束。
如果 DbgJITDebugLaunchSetting 被设置 1,那么程序会直接结束,并返回一个栈转储。
如果 DbgJITDebugLaunchSetting 被设置 2,那么将立刻启动在 DbgManagedDebugger 中指定的调试器,而不会显示消息框。
如果 DbgJITDebugLaunchSetting 被设置 16,对于交互式进程会显示前面的消息框,而对于非交互式进程则会直接启动在 DbgManagedDebugger 中指定的调试器。
B、眼见为实
调试源码:ExampleCore_8_01
调试任务:通过 Windows 调试器生成转储文件。
1)、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\bin\Debug\net8.0\ExampleCore_8_01.exe】打开调试器。
进入调试器,我们直接使用【g】命令,运行调试器,直到看到调试器输入“Press any key to start”字样,我们按回车键,让调试器继续执行,发现现在的调试器已经中断执行了。
我们可以执行【!clrstack】命令,查看一下当前的线程调用栈。
1 0:000> !clrstack 2 OS Thread Id: 0x1764 (0) 3 Child SP IP Call Site 4 000000132477E8A8 00007ff87ecbcf19 [HelperMethodFrame: 000000132477e8a8] 5 000000132477E9A0 00007FFF41F21AFB ExampleCore_8_01.Program.ProcessData(System.String) 6 000000132477EA00 00007FFF41F21A46 ExampleCore_8_01.Program.Run() 7 000000132477EA50 00007FFF41F21988 ExampleCore_8_01.Program.Main(System.String[]) 8 0:000>
接下来,我们使用【.dump /mf F:\Test\TestDump\08dumpfile2.dmp】命令,生成转储文件,保存目录在 F:\Test\TestDump 下,文件名称是 08dumpfile2.dmp。说明一下,文件名为什么加一个 2,因为我已经生成一个文件了,文件名必须不同,否则会有错误。
1 0:000> .dump /mf F:\Test\TestDump\08dumpfile.dmp 2 Unable to create file 'F:\Test\TestDump\08dumpfile.dmp' - Win32 error 0n80 3 "文件存在。"
文件名修改后,继续执行,看到“Dump successfully written”字样就说明成功了。
1 0:000> .dump /mf F:\Test\TestDump\08dumpfile2.dmp 2 Creating F:\Test\TestDump\08dumpfile2.dmp - mini user dump 3 Dump successfully written
我们需要再打开一个【NTSD】调试器,执行【NTSD -z F:\Test\TestDump\08dumpfile2.dmp】命令,加载我们刚刚生成的 DUMP 文件。
1 ** Visual Studio 2022 Developer Command Prompt v17.9.6 2 ** Copyright (c) 2022 Microsoft Corporation 3 ********************************************************************** 4 5 D:\Program Files\Microsoft Visual Studio\2022\Community>NTSD -z F:\Test\TestDump\08dumpfile2.dmp
调试器显示如下:
1 Microsoft (R) Windows Debugger Version 10.0.22621.2428 AMD64 2 Copyright (c) Microsoft Corporation. All rights reserved. 3 4 5 Loading Dump File [F:\Test\TestDump\08dumpfile2.dmp] 6 User Mini Dump File with Full Memory: Only application data is available 7 8 Symbol search path is: srv* 9 Executable search path is: 10 Windows 10 Version 19045 MP (4 procs) Free x64 11 Product: WinNt, suite: SingleUserTS 12 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406 13 Machine Name: 14 Debug session time: Wed Jun 26 17:02:22.000 2024 (UTC + 8:00) 15 System Uptime: 0 days 6:57:46.621 16 Process Uptime: 0 days 0:05:15.000 17 .................................. 18 This dump file has an exception of interest stored in it. 19 The stored exception information can be accessed via .ecxr. 20 (4fa8.1764): CLR exception - code e0434352 (first/second chance not available) 21 For analysis of this file, run !analyze -v 22 KERNELBASE!RaiseException+0x69: 23 00007ff8`7ecbcf19 0f1f440000 nop dword ptr [rax+rax]
第一部分红色标注的说明加载 DUMP 文件的信息,第二部分红色标注的就是故障原因(CLR 异常)。
接下来,我们就可以使用各种命令调试我们的程序了。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】调试器,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_8_01.exe,进入到调试器中。
直接【g】命令运行调试器,直到我们的控制台程序输入“Press any key to start”字样,我们在控制台程序中,按任何键继续执行。此时,调试器会中断执行,因为抛出了异常。
我们可以使用【!clrstack】命令查看一下调用栈的情况。
1 0:000> !clrstack 2 OS Thread Id: 0x3758 (0) 3 Child SP IP Call Site 4 000000DB2357E988 00007ff87ecbcf19 [HelperMethodFrame: 000000db2357e988] 5 000000DB2357EA80 00007fff3fb21afb ExampleCore_8_01.Program.ProcessData(System.String) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 22] 6 000000DB2357EAE0 00007fff3fb21a46 ExampleCore_8_01.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15] 7 000000DB2357EB30 00007fff3fb21988 ExampleCore_8_01.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8]
说明我们的程序执行到 ProcessData 方法的第 22 行出现了问题,因为我们抛出了异常。红色标注的 22 就是源代码中发生问题的行号。
接下来,我们使用【.dump /mf F:\Test\TestDump\08dumpfile.dmp】命令,生成转储文件,保存目录在 F:\Test\TestDump 下,文件名称是 08dumpfile.dmp。
1 0:000> .dump /mf F:\Test\TestDump\08dumpfile.dmp 2 Creating F:\Test\TestDump\08dumpfile.dmp - mini user dump 3 Dump successfully written
当我们看到了“Dump successfully written”字样时,说明 Dump 文件写成功了,在指定目录就能看到该文件。
我们有了 DUMP 文件,我们再打开一个【Windbg Preview】调试器加载 DUMP 文件就可以了。依次点击【文件】----【Open dump file】,在右侧通过浏览按钮选择我们的 Dump 文件,点击【open】按钮,就可以了。
1 ************* Preparing the environment for Debugger Extensions Gallery repositories ************** 2 ExtensionRepository : Implicit 3 UseExperimentalFeatureForNugetShare : false 4 AllowNugetExeUpdate : false 5 NonInteractiveNuget : true 6 AllowNugetMSCredentialProviderInstall : false 7 AllowParallelInitializationOfLocalRepositories : true 8 9 EnableRedirectToV8JsProvider : false 10 11 -- Configuring repositories 12 ----> Repository : LocalInstalled, Enabled: true 13 ----> Repository : UserExtensions, Enabled: true 14 15 >>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.000 seconds 16 17 ************* Waiting for Debugger Extensions Gallery to Initialize ************** 18 19 >>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.032 seconds 20 ----> Repository : UserExtensions, Enabled: true, Packages count: 0 21 ----> Repository : LocalInstalled, Enabled: true, Packages count: 41 22 23 Microsoft (R) Windows Debugger Version 10.0.27553.1004 AMD64 24 Copyright (c) Microsoft Corporation. All rights reserved. 25 26 27 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp] 28 User Mini Dump File with Full Memory: Only application data is available 29 30 31 ************* Path validation summary ************** 32 Response Time (ms) Location 33 Deferred srv* 34 Symbol search path is: srv* 35 Executable search path is: 36 Windows 10 Version 19045 MP (4 procs) Free x64 37 Product: WinNt, suite: SingleUserTS 38 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406 39 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00) 40 System Uptime: 0 days 3:36:45.501 41 Process Uptime: 0 days 0:16:15.000 42 ....................................................... 43 This dump file has an exception of interest stored in it. 44 The stored exception information can be accessed via .ecxr. 45 (43e0.3758): CLR exception - code e0434352 (first/second chance not available) 46 For analysis of this file, run !analyze -v 47 KERNELBASE!RaiseException+0x69: 48 00007ff8`7ecbcf19 0f1f440000 nop dword ptr [rax+rax]
第一部分红色标注的说明加载 DUMP 文件的信息,第二部分红色标注的就是故障原因(CLR 异常)。
接下来,我们就可以使用各种命令调试我们的程序了。
4.1.2、通过 ADPlus 生成转储文件
A、基础知识
ADPlus 工具能够监测和自动化一个或者多个故障进程的转储文件生成过程,并且能够在发生崩溃时通知用户或者计算机。ADPlus 工具位于 windbg 安装目录,最早叫 adplus.vbs,以VBScript脚本提供,最新版改成了adplus.exe。我们可以使用【cmd】命令打开命令行工具,直接输入 adplus 就可以运行,效果如下:
1 C:\Users\Administrator>adplus 2 Starting ADPlus 3 ******************************************************** 4 * * 5 * ADPLus Flash V 7.01.007 08/11/2011 * 6 * * 7 * For ADPlus documentation see ADPlus.doc * 8 * New command line options: * 9 * -pmn <procname> - process monitor * 10 * waits for a process to start * 11 * -po <procname> - optional process * 12 * won't fail if this process isn't running * 13 * -mss <LocalCachePath> * 14 * Sets Microsoft's symbol server * 15 * -r <quantity> <interval in seconds> * 16 * Runs -hang multiple times * 17 * * 18 * ADPlusManager - an additional tool to facilitate * 19 * the use of ADPlus in distributed environments like * 20 * computer clusters. * 21 * Learn about ADPlusManager in ADPlus.doc * 22 * * 23 ******************************************************** 24 25 26 ADPlus Version 7.01.007 08/11/2011 27 28 ==================== 29 | ADPlus Usage | 30 ==================== 31 Command line syntax options 32 ADPlus -? or 'ADPlus -help 33 Displays this information. 34 35 ADPlus -HelpConfig 36 Displays the built-in key-words and the default behavior settings 37 38 39 ADPlus <runmode> -o <OutputDirectory> [options] 40 Run Modes: 41 -Crash Runs ADPlus in Crash mode 42 -Hang Runs ADPlus in Hang mode 43 44 Selecting processes to attach 45 -p <PID> Defines a Process ID to be attached 46 -pn <ProcessName> Defines a process name to be attached 47 -po <ProcessName> Defines an optional process name to be attached 48 -pmn <ProcessName> Defines a process name to be monitored 49 ADPlus will keep monitoring if a process with this name starts 50 and attach 51 -sc <spawning command> Defines the application and parameters to be 52 started in the debugger 53 The -sc switch, if used, must be the last one 54 -iis All iis related processes will be attached 55 like inetinfo, dllhost,mtx, etc. 56 57 Symbol Path Options 58 -y <symbol path> Defines the symbol path to be used 59 -yp <symbol path to add> Defines an additional symbol path 60 -mss <local cache> Adds Microsoft Symbol Server to the symbol path 61 62 Memory Dump Options 63 -FullOnFirst Sets ADPlus to create full dumps on first chance exceptions 64 -MiniOnSecond Sets ADPlus to create mini dumps on second chance exceptions 65 -NoDumpOnFirst Sets ADPlus to not create any dumps on first chance exceptions 66 -NoDumpOnSecond Sets ADPlus to not create any dumps on second chance exceptions 67 -do Dump Only - changes default behavior to not include additional info, just a dump 68 69 Miscellaneous Options 70 -c <config file name> Defines a configuration file to be used 71 -o <output directory> Defines the directory where logs and dumps are 72 to be placed. 73 -r <quantity> <interval in seconds> for multiple attachments in hang mode 74 -dbg <debugger> Allows you to select the debugger to be used 75 cdb, windbg or ntsd (default is cdb) 76 -dp Debuggers path 77 -gs only generates the script file 78 79 -ce <custom exception code> Defines a custom exception to be monitored 80 -ce 0x80501001 81 82 -bp <breakpoint parameters> Sets a breakpoint 83 Syntax: -bp address;optional_additional_parameters 84 -bp MyModule!MyClass::MyMethod 85 -bp MyModule!MyClass::MyMethod;MiniDump 86 87 -CTCF Creates a full dump on CTL+C, and quits 88 -CTCFB Creates a full dump on CTL+C, and breaks into the debugger 89 -CTCV No special action on CTL+C, just breaks in for user interaction 90 -lcq sets the last script command to Q (quit) 91 -lcg sets the last script command to G (go) 92 -lcgn sets the last script command to GN (go not handled) 93 -lcqd sets the last script command to QD (quit and detach) 94 -lcv sets the last script command to void (no command; waits for user input) 95 -q2 sets the return action for second chance exceptions to Q (quit) 96 -g2 sets the return action for second chance exceptions to GN (go not handled) 97 98 99 -quiet No dialog boxes will be displayed (no more required) 100 -notify <destination> Will send a message to the destination 101 102 103 Examples: 104 ADPlus -hang -iis -o c:\dumps 105 Produces memory dumps of IIS and all 106 MTS/COM+ packages currently running. 107 108 ADPlus -crash -p 1896 -o c:\dumps -mss c:\symbols 109 Attaches the debugger to process with PID 1896 110 and monitors it for 1st and 2nd chance access violations and uses 111 Microsoft's public symbol server with c:\symbols as a local cache 112 113 ------------------------------------------------------------------------------- 114 115 HELP and Documentation 116 117 For more detailed information on how to use and config ADPlus please see 118 the debugger's help file (debugger.chm) under Extra Tools 119 However, be aware that this is a new version of ADPlus and debugger.chm 120 may take some time to be updated 121 Check for ADPlus.doc in the debuggers' folder 122 ------------------------------------------------------------------------------- 123 Current log content 124 125 ADPlus Engine Version: 7.01.007 08/11/2011 126 Command line arguments used were:
ADPlus.exe 不仅可以在程序崩溃时手动运行来生成dmp文件,也可以在崩溃之前就运行它,当程序崩溃时它会自动生成dmp文件;甚至可以在程序没有运行之前就先运行adplus,当程序崩溃时它会自动生成dmp文件。
ADPlus 可以在以下两种模式下运行:
1)、挂起模式(Hang Mode):用于分析出现挂起现象的进程(例如:程序不执行或者 100% 的 CPU 使用率)。ADPlus 必须在进程挂起之后启动。
2)、崩溃模式(Crash Mode):用于分析出现崩溃行为的进程。ADPlus 必须在进程崩溃之后启动。
ADPlus 可以控制生成转储文件的类型,共有四个命令行开关控制这种行为:
1)、-FullOnFirst:将 ADPlus 设置为在首次出现异常时创建完整转储文件。
2)、-MiniOnSecond:将 ADPlus 设置为在第二次出现异常时创建微型转储文件。
3)、-NoDumpOnFirst:将 ADPlus 设置为在首次出现异常时不创建任何微型转储文件。
4)、-NoDumpOnSecond:将 ADPlus 设置为在第二次出现异常时不创建任何微型转储文件。
ADPlus 还为用户提供了一种功能强大的方式来配置信息收集的频率以及在何种条件下收集信息,尤其是为调试器提供了一个脚本前端。这样做不过是采用了一种对用户更友好的方式来执行调试器命令,并将它们的执行过程自动化。
如果我们想了解 ADPlus 命令的使用,可以使用【adplus -?】命令查看该命令的使用方法和各种参数。
B、眼见为实
调试源码:ExampleCore_8_01
调试任务:通过 ADPlus 生成转储文件。
1)、崩溃模式
首先,我们先编译我们的项目,打开项目的可以执行程序文件,也就是 EXE 文件,直接双击执行。我们的程序输出“Press any key to start”字样。
在继续之前,我们打开【Visual Studio 2022 Developer Command Prompt v17.9.6】或者【cmd】命令行工具都可以,执行【adplus -pn ExampleCore_8_01.exe -crash -o F:\Test\TestDump】命令,-crash 将 ADPlus 设置为崩溃模式,-pn 告诉 ADPlus 要监控的进程名称,该参数的好处是它能够监视由 name 指定的进程的任意数量实例。-o 表示文件存储的目录地址。
执行效果如图:
当执行完成后,ADPlus 会把结果日志文件保存到我们设置的目录。效果如图:
它会新建一个目录 20240627_152459_Crash_Mode,在该目录下存放日志文件。当我们的进程发生关闭事件时,ADPlus 将生成一个完全转储文件,我的文件名是:FULLDUMP_SecondChance_clr_NET_CLR_ExampleCore_8_01.exe__3c04_2024-06-27_16-15-43-200_2c28.dmp,效果如图:
文件名太长,截图没有显示全部。我们看到在 F:\Test\TestDump\20240627_152459_Crash_Mode 目录下,有一个文件 DebuggerScript.txt,其中包含了在 ADPlus 会话中使用的所有调试器命令。
4.1.3、转储文件的调试
因为转储文件只是进程状态的一个静态快照,因此,我们不能在代码上设置断点以及单步调试。最好把转储文件看成是一种手动调试,在使用转储文件时,仍然可以使用大多数的调试器命令。
在准备调试转储文件之前,需要先获取两个关键信息:符号文件和数据访问层(Data Access Layer,DAC)。由于在转储文件中不包含任何符号信息,因此当分析转储文件时,符号文件是非常重要的。这里的数据访问层(DAC)指的是 CLR 数据访问层,SOS 将通过这个信息来提供在调试会话中需要的数据。
4.1.4、数据访问层
先说明一下:对于所有版本的.NET Framework,DAC 的文件名 mscordacwks.dll,SOS 调试扩展的文件名 sos.dll,在 Net 跨平台版本名称改了,名称是 mscordaccore.d。
我先说说在 Net Framework 环境下的情况。在非托管调试环境中,许多信息都可以通过观察内存来收集,而在托管代码中,SOS 依靠 CLR 来提供我们的调试输出以及结果。为了使 SOS 能够正确解析传递给它的原始数据,SOS 将调用 CLR (即执行CLR代码)来辅助执行这个过程。CLR 中负责实现这个功能的组件就是数据访问层,它包含在 mscordacwks.dll 中。现在,随着 CLR 被不断地增强,底层的 DAC 同样随各个版本(包含补丁)的不同而变化。通过查看机器上每个.NET版本的安装文件夹可以很容易地验证这一点。例如,在我的机器上,mscordacwks.dll 位于以下文件夹中:C:\Windows\Microsoft.NET\Framework\v4.0.30319。效果如图:
64 位的目录:C:\Windows\Microsoft.NET\Framework64\v4.0.30319,效果如图:
由于调试器在其操作期间需要用到这个组件,因此要知道调试器这个文件的位置。在实时调试过程通常不需要关心这个问题,因为 SOS 能够从当前被调试的 CLR 所在位置上找到这个文件。在事后调试(或者转储文件)中,在程序中使用的 CLR 版本可能与转储文件所在机器上的 CLR 版本不同。再次重申,SOS 调试器扩展将调用 mscordacwks.dll 中的函数,这个动态库将执行 CLR 代码,因此为调试器指定正确的版本是非常重要的。
由于 CLR 版本的正确与否对于调试是否成功至关重要,因此,微软公布了 mscordacwks.dll 大部分符号,放在 Microsoft 共有符号服务器上。只要将调试器的符号路径指向共有符号服务器(使用 symfix 或者其他相关的命令),那么调试器就能找到这个文件。但是,有时候需要我们显示告诉 SOS 扩展命令在什么位置上查找该文件。这些情况:当文件不在公共符号服务器上或者文件没有安装在与生成转储文件的机器上的同一个路径下。此时,我们就可以使用【.cordll】命令来控制 mscordacwks.dll 加载的方式,并在处理版本不匹配问题时节约大量的时间。
接下来,我们看看【.cordll】命令的开关:
1)、-l 在默认加载路径中搜索 DLL并加载调试模块。
2)、-u 从内存中卸载调试模块。
3)、-e 启用 CLR 调试。
4)、-d 禁用 CLR 调试
5)、-D 禁用 CLR 调试并卸载调试模块。
6)、-N 重新加载调试模块。
7)、-lp 指定调试模块的目录。
8)、-se 启用使用短名字版本的调试模块,mscordacwks.dll。
9)、-sd 禁用使用短名字的调试模块,mscordacwks.dll。如果指定了这个开关,那么调试模块要以以下格式
加载:mscordacwks_<spec>.dll,其中<spec>的形式为<architecture>_<architecture ><file version>,而<Architecture >可以是x86 或者amd64。
10)、-ve 启用 verbose 模式。当处理不匹配问题时,verbose 模式是非常有用的,因为它能给出调试器如何加载调试模块的信息
11)、-vd 禁用 verbose 模式。
接下来,我们开始讲讲在 Net 5.0 和以上版本的情况,包括 NET 6.0、NET 7.0、NET 8.0,以后还会有更新的版本,都包含在内。
在 NET 跨平台版本中,名称和文件存放位置都发生了很大的变化,现在的文件名称是:mscordaccore.dll。当然这个文件也有两个版本,一个是 x86 架构的,一个是 64 架构的。我们安装几个版本的 .NET runtime,就会有几个版本的 DAC 文件与之匹配。我们可以使用【cmd】命令,打开命令行工具,然后输入【dotnet --info】命令,查看 dotnet 具体详情。
1 C:\Users\Administrator>dotnet --info 2 .NET SDK: 3 Version: 8.0.204 4 Commit: c338c7548c 5 Workload version: 8.0.200-manifests.7d36c14f 6 7 运行时环境: 8 OS Name: Windows 9 OS Version: 10.0.19045 10 OS Platform: Windows 11 RID: win-x64 12 Base Path: C:\Program Files\dotnet\sdk\8.0.204\ 13 14 已安装 .NET 工作负载: 15 没有要显示的已安装工作负载。 16 17 Host: 18 Version: 8.0.4 19 Architecture: x64 20 Commit: 2d7eea2529 21 22 .NET SDKs installed: 23 6.0.402 [C:\Program Files\dotnet\sdk] 24 7.0.302 [C:\Program Files\dotnet\sdk] 25 8.0.201 [C:\Program Files\dotnet\sdk] 26 8.0.204 [C:\Program Files\dotnet\sdk] 27 28 .NET runtimes installed: 29 Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 30 Microsoft.AspNetCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 31 Microsoft.AspNetCore.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 32 Microsoft.AspNetCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 33 Microsoft.AspNetCore.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 34 Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 35 Microsoft.AspNetCore.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 36 Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 37 Microsoft.NETCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 38 Microsoft.NETCore.App 6.0.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 39 Microsoft.NETCore.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 40 Microsoft.NETCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 41 Microsoft.NETCore.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 42 Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 43 Microsoft.NETCore.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 44 Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 45 Microsoft.WindowsDesktop.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 46 Microsoft.WindowsDesktop.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 47 Microsoft.WindowsDesktop.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 48 Microsoft.WindowsDesktop.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 49 Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 50 Microsoft.WindowsDesktop.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 51 52 Other architectures found: 53 x86 [C:\Program Files (x86)\dotnet] 54 registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation] 55 56 Environment variables: 57 Not set 58 59 global.json file: 60 Not found 61 62 Learn more: 63 https://aka.ms/dotnet/info 64 65 Download .NET: 66 https://aka.ms/dotnet/download
这个命令的输出内容确实很多,加粗标注的就是【Runtime(运行时)】的版本,我们也可以使用【dotnet --list-runtimes】命令,只查看运行时。
1 C:\Users\Administrator>dotnet --list-runtimes 2 Microsoft.AspNetCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 3 Microsoft.AspNetCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 4 Microsoft.AspNetCore.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 5 Microsoft.AspNetCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 6 Microsoft.AspNetCore.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 7 Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 8 Microsoft.AspNetCore.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] 9 Microsoft.NETCore.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 10 Microsoft.NETCore.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 11 Microsoft.NETCore.App 6.0.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 12 Microsoft.NETCore.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 13 Microsoft.NETCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 14 Microsoft.NETCore.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 15 Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 16 Microsoft.NETCore.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] 17 Microsoft.WindowsDesktop.App 3.1.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 18 Microsoft.WindowsDesktop.App 6.0.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 19 Microsoft.WindowsDesktop.App 6.0.29 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 20 Microsoft.WindowsDesktop.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 21 Microsoft.WindowsDesktop.App 7.0.18 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 22 Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] 23 Microsoft.WindowsDesktop.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
这些【运行时(Runtimes)】的目录是【C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App】,说明是 64位架构的。说明一下,在这个目录下是找不到 mscordaccore.dll 文件的,该文件的目录是:C:\Program Files\dotnet\shared\Microsoft.NETCore.App,桌面程序(WindowsDesktop)和Asp.Net 程序(AspNetCore)共用这个。效果如图:
随便打开一个文件夹下,都能找到 mscordaccore.dll 和 coreclr.dll 文件,我打开 8.0.4 文件夹,效果如图:
以上是 64 位架构的,接下来,我们看看 x86 架构的,该文件的地址目录是:C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App。
效果如图:
在这里,我同样打开最新版本的文件夹,该文件夹名称是 8.0.4,效果如图:
如何知道是否需要使用【.cordll】命令?如果存在版本不匹配的 mscordaccore.dll(在 Net Framework 版本里提示的是 mscordacwks.dll) ,那么 SOS 调试扩展将输出以下错误信息,在跨平台版本里 mscordaccore.dll 和 coreclr.dll 是一一对应的,在 .Net Framework 版本里 mscordacwks.dll 和 mscorwks.dll 是一一对应的。
说明一下,我已经加载了一个 DUMP 文件,只是执行了一个【!t】命令,就会输出一下内容:
1 0:000> !t 2 Failed to load data access module, 0x80004002 3 Verify that 1) you have a recent build of the debugger (10.0.18317.1001 or newer) 4 2) the file mscordaccore.dll that matches your version of coreclr.dll is 5 in the version directory or on the symbol path 6 3) or, if you are debugging a dump file, verify that the file 7 mscordaccore_<arch>_<arch>_<version>.dll is on your symbol path. 8 4) you are debugging on a platform and architecture that supports this 9 the dump file. For example, an ARM dump file must be debugged 10 on an X86 or an ARM machine; an AMD64 dump file must be 11 debugged on an AMD64 machine. 12 13 You can run the command '!setclrpath <directory>' to control the load path of mscordaccore.dll. 14 15 Or you can also run the debugger command .cordll to control the debugger's 16 load of mscordaccore.dll. .cordll -ve -u -l will do a verbose reload. 17 If that succeeds, the SOS command should work on retry. 18 19 If you are debugging a minidump, you need to make sure that your executable 20 path is pointing to coreclr.dll as well. 21 22 For more information see https://go.microsoft.com/fwlink/?linkid=2135652
我们仔细看看这篇建议,第一建议很简单,就是要确保我们的调试器是最新版本。第二条建议是要确保 mscordaccore.dll(原著中是 mscordacwks.dll,也就是 NET FRAMEWORK 版本)的版本和所加载的 coreclr.dll(原著中的是 mscorwks.dll,也就是 NET FRAMEWORK 版本)是相匹配的。第三条建议是如果你调试的 DUMP 文件,要确保 mscordaccore_<arch>_<arch>_<version>.dll 文件位于符号路径中。这个名字其实就是使用了 -sd 命令开关启用长名字,长名字只是将这个 DLL 文件对应的架构以及构建编号添加到 DLL 的名字中。然后,就可以更新符号路径,指向这个 DLL,并执行【.cordll】命令来重新加载 mscordaccore.dll。
例如:如果在生成转储文件时使用的是 mscordaccore.dll 的版本为 1.1.1.0,架构为 x86,那么就可以将 mscordaccore.dll 重命名为 mscordaccore_x86_x86_1.1.1.0.dll,并将调试器的符号路径指向这个重命名的位置,接着使用【.cordll】命令来重新加载调试模块。效果如图:
第四条建议确保运行调试器的所在的架构与生成转储文件的架构相同。由于调试器要执行 DAC 中的代码,因此,用于调试转储文件的调试器的架构信息与创建转储文件时使用的调试器架构信息要完全一样。
输出的最后一行,要求可执行路径指向 mscordaccore.dll。可执行路径可以在调试器中通过【.exepath】命令来控制。如果要添加可执行路径,可以使用【!exepath +】命令。
如果无法找到在生成转储文件时使用的 DLL 正确版本,最简单的方法就是,要求生成转储文件的人员把相应的 mscordaccore.dll 文件发给你,在收到文件后,在按照之前给出的策略来加载它。在成功加载之后,SOS 调试器扩展会充分发挥其功能。
我们可以使用【Windbg Preview】调试器,输入【.cordll】命令,可以加载 mscordaccore.dll。
1 0:000> .cordll 2 CLR DLL status: Loaded DLL C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\mscordaccore.dll
4.1.5、转储文件分析:未处理的 .NET 异常。
调试源码:根据 ExampleCore_8_01 生成的 DUMP 文件。
调试任务:使用调试器分析 DUMP 文件。
1)、NTSD 调试
打开【Visual Studio 2022 Developer Command Prompt v17.9.6】,输入命令【NTSD -z F:\Test\TestDump\08dumpfile.dmp】打开调试器。
我们成功打开调试器,并且调试器已经中断执行,因为出现了异常。
1 Microsoft (R) Windows Debugger Version 10.0.22621.2428 AMD64 2 Copyright (c) Microsoft Corporation. All rights reserved. 3 4 5 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp]--成功加载我们的 DUMP 文件 6 User Mini Dump File with Full Memory: Only application data is available 7 8 Symbol search path is: srv* 9 Executable search path is: 10 Windows 10 Version 19045 MP (4 procs) Free x64 11 Product: WinNt, suite: SingleUserTS 12 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406 13 Machine Name: 14 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00) 15 System Uptime: 0 days 3:36:45.501 16 Process Uptime: 0 days 0:16:15.000 17 ................................... 18 This dump file has an exception of interest stored in it. 19 The stored exception information can be accessed via .ecxr. 20 (43e0.3758): CLR exception - code e0434352 (first/second chance not available)发生了异常 21 For analysis of this file, run !analyze -v 22 KERNELBASE!RaiseException+0x69: 23 00007ff8`7ecbcf19 0f1f440000 nop dword ptr [rax+rax]
以上信息说明,这个 DUMP 是由于引发了一个 CLR 异常而生成的。接着,我们查看异常的详细信息,包括传递的参数,使用【kb】命令。
1 0:000> kb 2 RetAddr : Args to Child : Call Site 3 00007fff`9f5eb3a3 : 000001e5`f8809ce8 000000db`2357e7d0 00007fff`3fbe1080 00007fff`3fbd5e00 : KERNELBASE!RaiseException+0x69(抛出异常) 4 00007fff`9f5ead49 : 00000000`00000004 00000000`00000001 000000db`2357ec18 000000db`2357edb8 : coreclr!RaiseTheExceptionInternalOnly+0x26b 5 00007fff`3fb21afb : 000001e5`f8809ce8 00000226`8aad0a38 000001e5`f44cb1e8 00000226`8aae0000 : coreclr!IL_Throw 6 00007fff`3fb21a46 : 000001e5`f8809630 00000000`00000000 000001e5`f44af4d0 000001e5`f8809c40 : 0x00007fff`3fb21afb 7 00007fff`3fb21988 : 000001e5`f8809630 000001e5`f8809648 000000db`2357f1b8 000000db`2357eda9 : 0x00007fff`3fb21a46 8 00007fff`9f68b8d3 : 000001e5`f8808e98 000000db`2357f1b8 000000db`2357f1b8 000000db`2357eda9 : 0x00007fff`3fb21988 9 00007fff`9f5c0b19 : 00000000`00000000 00000000`00000130 000000db`2357edb8 00007fff`9f540232 : coreclr!CallDescrWorkerInternal+0x83 10 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 11 00007fff`9f5bd730 : 000000db`2357ee38 00000000`00000000 00000000`00000048 00007fff`9f62d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 12 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb 13 00007fff`9f5e2fc6 : 000001e5`f8808e98 000001e5`f8808e98 00000000`00000000 000000db`2357f1b8 : coreclr!RunMainInternal+0x11c 14 00007fff`9f5e32fb : 000001e5`f44af4d0 000001e5`00000000 000001e5`f44af4d0 00000000`00000000 : coreclr!RunMain+0xd2 15 00007fff`9f539141 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf 16 00007fff`9f64e8b8 : 00000000`00000001 000000db`2357f301 000000db`2357f3e0 00007fff`9fa223ea : coreclr!CorHost2::ExecuteAssembly+0x281 17 00007fff`9fa42b76 : 000001e5`f4481250 000001e5`f4481030 00000000`00000000 000001e5`f4481030 : coreclr!coreclr_execute_assembly+0xd8 18 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a 19 00007fff`9fa42e5c : 000001e5`f446de98 000000db`2357f609 00007fff`9fa7ca10 000001e5`f446de98 : hostpolicy!run_app_for_context+0x596 20 00007fff`9fa4379a : 00000000`00000000 000001e5`f446de90 000001e5`f446de90 00000000`00000000 : hostpolicy!run_app+0x3c 21 00007fff`9fa9b5c9 : 000001e5`f447e468 000001e5`f447e350 00000000`00000000 000000db`2357f709 : hostpolicy!corehost_main+0x15a 22 00007fff`9fa9e066 : 000001e5`f447ddc0 000000db`2357fa90 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 23 00007fff`9faa02ec : 00007fff`9fad25f8 000001e5`f447c530 000000db`2357f9d0 000000db`2357f980 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 24 00007fff`9fa9e644 : 000000db`2357fa90 000000db`2357fab0 000000db`2357fa01 000001e5`f447c9a0 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c 25 00007fff`9fa985a0 : 000000db`2357fab0 000001e5`f447fdf0 00000000`00000001 000001e5`f4460000 : hostfxr!fx_muxer_t::execute+0x494 26 *** WARNING: Unable to verify checksum for ExampleCore_8_01.exe 27 00007ff7`ae24f998 : 00007ff8`7f40f4e8 00007fff`9fa99b10 000000db`2357fc50 000001e5`f447bf30 : hostfxr!hostfxr_main_startupinfo+0xa0 28 00007ff7`ae24fda6 : 00007ff7`ae25b6c0 00000000`00000007 000001e5`f446de90 00000000`0000005e : ExampleCore_8_01_exe!exe_start+0x878 29 00007ff7`ae2512e8 : 00000000`00000000 00000000`00000000 000001e5`f446de90 00000000`00000000 : ExampleCore_8_01_exe!wmain+0x146 30 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : ExampleCore_8_01_exe!invoke_main+0x22 31 00007ff8`801d7344 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ExampleCore_8_01_exe!__scrt_common_main_seh+0x10c 32 00007ff8`816026b1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14 33 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
我们看堆栈知道最后抛出异常,红色标注的地址就是异常对象的具体地址。我们可以使用【!do 000001e5`f8809ce8】命令或者【!DumpObj 000001e5`f8809ce8】,这个命令输入的内容太多。
1 0:000> !do 000001e5`f8809ce8 2 Name: System.ArgumentNullException(空引用异常,就是我们抛出的) 3 MethodTable: 00007fff3fc22cb0 4 EEClass: 00007fff3fbef210 5 Tracked Type: false 6 Size: 136(0x88) bytes 7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll 8 Fields: 9 MT Field Offset Type VT Attr Value Name 10 00007fff3fc77688 4000264 8 ...ection.MethodBase 0 instance 0000000000000000 _exceptionMethod 11 00007fff3fb0ec08 4000265 10 System.String 0 instance 000001e5f880d328 _message 12 00007fff3fba3060 4000266 18 ...tions.IDictionary 0 instance 0000000000000000 _data 13 00007fff3fba0820 4000267 20 System.Exception 0 instance 0000000000000000 _innerException 14 00007fff3fb0ec08 4000268 28 System.String 0 instance 0000000000000000 _helpURL 15 00007fff3fc09d98 4000269 30 System.Byte[] 0 instance 000001e5f880d4c8 _stackTrace 16 00007fff3fc09d98 400026a 38 System.Byte[] 0 instance 0000000000000000 _watsonBuckets 17 00007fff3fb0ec08 400026b 40 System.String 0 instance 0000000000000000 _stackTraceString 18 00007fff3fb0ec08 400026c 48 System.String 0 instance 0000000000000000 _remoteStackTraceString 19 00007fff3fa5c4d8 400026d 50 System.Object[] 0 instance 0000000000000000 _dynamicMethods 20 00007fff3fb0ec08 400026e 58 System.String 0 instance 0000000000000000 _source 21 00007fff3fb08b78 400026f 60 System.UIntPtr 1 instance 00007FFF3FB21AFA _ipForWatsonBuckets 22 00007fff3fb070a0 4000270 68 System.IntPtr 1 instance 0000000000000000 _xptrs 23 00007fff3fa91188 4000271 70 System.Int32 1 instance -532462766 _xcode 24 00007fff3fa91188 4000272 74 System.Int32 1 instance -2147467261 _HResult 25 00007fff3fb0ec08 4000383 78 System.String 0 instance 000002268aad0a38 _paramName
我们也可以使用【!pe】命令,这个命令输出就很简洁了。
1 0:000> !pe 000001e5`f8809ce8 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: 000001e5f8809ce8 6 Exception type: System.ArgumentNullException 7 Message: Value cannot be null. 8 InnerException: <none> 9 StackTrace (generated): 10 SP IP Function 11 000000DB2357EA80 00007FFF3FB21AFB ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData(System.String)+0x8b 12 000000DB2357EAE0 00007FFF3FB21A46 ExampleCore_8_01!ExampleCore_8_01.Program.Run()+0x46 13 000000DB2357EB30 00007FFF3FB21988 ExampleCore_8_01!ExampleCore_8_01.Program.Main(System.String[])+0x58 14 15 StackTraceString: <none> 16 HResult: 80004003
在这里看的就很清楚了,异常类型,错误码和调用堆栈都是一目了然。
2)、Windbg Preview 调试
我们打开【Windbg Preview】调试器,依次点击【文件】----【Open dump file】,在右侧选择我们的 Dump 文件,点击【open】按钮,就打开了调试器。
由于我们调试的是 Dump 文件,所以调试刚开始的输出也是不一样的。
1 ************* Preparing the environment for Debugger Extensions Gallery repositories ************** 2 ExtensionRepository : Implicit 3 UseExperimentalFeatureForNugetShare : false 4 AllowNugetExeUpdate : false 5 NonInteractiveNuget : true 6 AllowNugetMSCredentialProviderInstall : false 7 AllowParallelInitializationOfLocalRepositories : true 8 9 EnableRedirectToV8JsProvider : false 10 11 -- Configuring repositories 12 ----> Repository : LocalInstalled, Enabled: true 13 ----> Repository : UserExtensions, Enabled: true 14 15 >>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.000 seconds 16 17 ************* Waiting for Debugger Extensions Gallery to Initialize ************** 18 19 >>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.031 seconds 20 ----> Repository : UserExtensions, Enabled: true, Packages count: 0 21 ----> Repository : LocalInstalled, Enabled: true, Packages count: 41 22 23 Microsoft (R) Windows Debugger Version 10.0.27553.1004 AMD64 24 Copyright (c) Microsoft Corporation. All rights reserved. 25 26 27 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp] 28 User Mini Dump File with Full Memory: Only application data is available 29 30 31 ************* Path validation summary ************** 32 Response Time (ms) Location 33 Deferred srv* 34 Symbol search path is: srv* 35 Executable search path is: 36 Windows 10 Version 19045 MP (4 procs) Free x64 37 Product: WinNt, suite: SingleUserTS 38 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406 39 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00) 40 System Uptime: 0 days 3:36:45.501 41 Process Uptime: 0 days 0:16:15.000 42 ....................................................... 43 This dump file has an exception of interest stored in it. 44 The stored exception information can be accessed via .ecxr. 45 (43e0.3758): CLR exception - code e0434352 (first/second chance not available) 46 For analysis of this file, run !analyze -v 47 KERNELBASE!RaiseException+0x69: 48 00007ff8`7ecbcf19 0f1f440000 nop dword ptr [rax+rax]
红色标注的都说明了加载了我们的 Dump 文件,并且由于发生了一个 CLR 异常生成的转储文件。
我们可以使用【!clrstack】命令看看托管调用堆栈,这里有用的信息不多。
1 0:000> !clrstack 2 OS Thread Id: 0x3758 (0) 3 Child SP IP Call Site 4 000000DB2357E988 00007ff87ecbcf19 [HelperMethodFrame: 000000db2357e988] 5 000000DB2357EA80 00007fff3fb21afb ExampleCore_8_01.Program.ProcessData(System.String) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 22] 6 000000DB2357EAE0 00007fff3fb21a46 ExampleCore_8_01.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15] 7 000000DB2357EB30 00007fff3fb21988 ExampleCore_8_01.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8]
我们可以使用【kb】命令查看非托管调用栈,包括参数。
1 0:000> kb 2 *** WARNING: Unable to verify checksum for ExampleCore_8_01.exe 3 # RetAddr : Args to Child : Call Site 4 00 00007fff`9f5eb3a3 : 000001e5`f8809ce8 000000db`2357e7d0 00007fff`3fbe1080 00007fff`3fbd5e00 : KERNELBASE!RaiseException+0x69 5 01 00007fff`9f5ead49 : 00000000`00000004 00000000`00000001 000000db`2357ec18 000000db`2357edb8 : coreclr!RaiseTheExceptionInternalOnly+0x26b [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 2795] 6 02 00007fff`3fb21afb : 000001e5`f8809ce8 00000226`8aad0a38 000001e5`f44cb1e8 00000226`8aae0000 : coreclr!IL_Throw+0xb9 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 4247] 7 03 00007fff`3fb21a46 : 000001e5`f8809630 00000000`00000000 000001e5`f44af4d0 000001e5`f8809c40 : ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData+0x8b [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 24] 8 04 00007fff`3fb21988 : 000001e5`f8809630 000001e5`f8809648 000000db`2357f1b8 000000db`2357eda9 : ExampleCore_8_01!ExampleCore_8_01.Program.Run+0x46 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15] 9 05 00007fff`9f68b8d3 : 000001e5`f8808e98 000000db`2357f1b8 000000db`2357f1b8 000000db`2357eda9 : ExampleCore_8_01!ExampleCore_8_01.Program.Main+0x58 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8] 10 06 00007fff`9f5c0b19 : 00000000`00000000 00000000`00000130 000000db`2357edb8 00007fff`9f540232 : coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100] 11 07 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67] 12 08 00007fff`9f5bd730 : 000000db`2357ee38 00000000`00000000 00000000`00000048 00007fff`9f62d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 570] 13 09 (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb [D:\a\_work\1\s\src\coreclr\vm\callhelpers.h @ 458] 14 0a 00007fff`9f5e2fc6 : 000001e5`f8808e98 000001e5`f8808e98 00000000`00000000 000000db`2357f1b8 : coreclr!RunMainInternal+0x11c [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1304] 15 0b 00007fff`9f5e32fb : 000001e5`f44af4d0 000001e5`00000000 000001e5`f44af4d0 00000000`00000000 : coreclr!RunMain+0xd2 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1375] 16 0c 00007fff`9f539141 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1504] 17 0d 00007fff`9f64e8b8 : 00000000`00000001 000000db`2357f301 000000db`2357f3e0 00007fff`9fa223ea : coreclr!CorHost2::ExecuteAssembly+0x281 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349] 18 0e 00007fff`9fa42b76 : 000001e5`f4481250 000001e5`f4481030 00000000`00000000 000001e5`f4481030 : coreclr!coreclr_execute_assembly+0xd8 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 504] 19 0f (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 109] 20 10 00007fff`9fa42e5c : 000001e5`f446de98 000000db`2357f609 00007fff`9fa7ca10 000001e5`f446de98 : hostpolicy!run_app_for_context+0x596 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256] 21 11 00007fff`9fa4379a : 00000000`00000000 000001e5`f446de90 000001e5`f446de90 00000000`00000000 : hostpolicy!run_app+0x3c [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285] 22 12 00007fff`9fa9b5c9 : 000001e5`f447e468 000001e5`f447e350 00000000`00000000 000000db`2357f709 : hostpolicy!corehost_main+0x15a [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426] 23 13 00007fff`9fa9e066 : 000001e5`f447ddc0 000000db`2357fa90 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145] 24 14 00007fff`9faa02ec : 00007fff`9fad25f8 000001e5`f447c530 000000db`2357f9d0 000000db`2357f980 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532] 25 15 00007fff`9fa9e644 : 000000db`2357fa90 000000db`2357fab0 000000db`2357fa01 000001e5`f447c9a0 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007] 26 16 00007fff`9fa985a0 : 000000db`2357fab0 000001e5`f447fdf0 00000000`00000001 000001e5`f4460000 : hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578] 27 17 00007ff7`ae24f998 : 00007ff8`7f40f4e8 00007fff`9fa99b10 000000db`2357fc50 000001e5`f447bf30 : hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62] 28 18 00007ff7`ae24fda6 : 00007ff7`ae25b6c0 00000000`00000007 000001e5`f446de90 00000000`0000005e : ExampleCore_8_01_exe!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240] 29 19 00007ff7`ae2512e8 : 00000000`00000000 00000000`00000000 000001e5`f446de90 00000000`00000000 : ExampleCore_8_01_exe!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311] 30 1a (Inline Function) : --------`-------- --------`-------- --------`-------- --------`-------- : ExampleCore_8_01_exe!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90] 31 1b 00007ff8`801d7344 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ExampleCore_8_01_exe!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 32 1c 00007ff8`816026b1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14 33 1d 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
000001e5`f8809ce8 这个参数就是异常实例的地址,我们可以使用【!do 000001e5`f8809ce8】命令查看,但是使用【!pe 000001e5`f8809ce8】命令会更好。
1 0:000> !pe 000001e5`f8809ce8 2 Exception object: 000001e5f8809ce8 3 Exception type: System.ArgumentNullException 4 Message: Value cannot be null. 5 InnerException: <none> 6 StackTrace (generated): 7 SP IP Function 8 000000DB2357EA80 00007FFF3FB21AFB ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData(System.String)+0x8b 9 000000DB2357EAE0 00007FFF3FB21A46 ExampleCore_8_01!ExampleCore_8_01.Program.Run()+0x46 10 000000DB2357EB30 00007FFF3FB21988 ExampleCore_8_01!ExampleCore_8_01.Program.Main(System.String[])+0x58 11 12 StackTraceString: <none> 13 HResult: 80004003
这里的信息就很清楚了,异常类型,调用堆栈一目了然。
4.2、Windows 错误报告
这一节的内容挺不好写的,因为这里面的内容发生了很大的变化,如果照着原文写,很多内容会过时的,所以,这节就简写了,我会给出微软官方的有关文章的链接,这个链接放在了本节的最后,大家可以去学习。
Windows 错误报告(Windows Error Reporting,WER)是一种聚合故障数据的服务,使得 Microsoft 和独立软件供应商(Independent Software Vendor,ISV)可以很容易的访问与他们程序相关的故障数据。
我们先上一个图,来说明一下 WER 服务的操作流程。
假设在世界的某个地方,有一台计算机正在运行由 ADND 企业开发的一个程序(就是图中的 X 进程)。假设这个程序崩溃了,并且用户看到了 Dr.Watson 界面并询问是否将错误报告发送给 Microsoft。用户选择了发送,并且错误报告将通过安全通道(HTTPS)发送给 WER 服务。然后,WER 将收到错误报告进行分门别类并保存。要使用这些错误报告,来之 ADND 企业的用户需要查询 WER 服务,找出与其程序相关的崩溃并且获得报告的错误信息。如果 ADND 得到了这些错误报告,那么就可以修正这个问题,并且提供一个回应,这样下一次当用户遇到了想通的崩溃情况时,Dr. Watson 将给出相应的回应。这个回应可以是一个补丁或者是其他一些帮助信息。
WER 服务是一种功能非常强大的机制,提供了对错误报告的聚合功能,ISV 可以查询这些信息来改进程序。此外,ISV 可以提供问题的回应,并且这个回应会集成到 WER 反馈循环中,从而可以使用户很容易得到回应。
要想使用 WER 服务,必须向 Windows 错误报告服务注册,剩下的内容就是如何注册账号和使用了,内容太老就不多说了。
在文章的最后,如果大家想了解 WER 最新的使用方法,我把 Microsoft 官网文章地址贴出来,大家可以自行学习。
地址如下:https://learn.microsoft.com/zh-cn/windows/win32/wer/windows-error-reporting
五、总结
这篇文章的终于写完了,这篇文章的内容相对来说,不是很多。写完一篇,就说明进步了一点点。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。