内存转储是查明托管.NET应用程序中异常的原因的一种极好的方法,特别是在生产应用程序中发生异常时。当您在无法使用Visual Studio的应用程序中跟踪异常时,cdb和sos.dll的使用技术就变成了它们自己的技术。它们可能也不是你需要经常使用的技能,但在某些时候,它们将是无价的。Edward提供了内存转储示例,并为您提供了一个简单的介绍。
Windows的调试工具占主导地位,sos.dll将CLR内部变成了一个真正的知识源泉。具体来说,我将讨论现有的异常处理框架,讨论.Net异常,如System.exception,以及我所称的“CPU异常”,如访问冲突(C0000005):您可能知道它们是Win32或硬件异常。然后,我将解释如何创建和使用内存转储来查找和修复事后的错误;换句话说,事后调试。 最后,我将展示一个使用cdb和Visual Studio调试的示例。在第一篇文章中(不要给我任何VS),我提倡使用cdb,但是我现在意识到这是一个很大的飞跃:因此,在本文中,我将展示一个使用cdb和sos.dll的示例,然后继续讨论如何在Visual Studio中实现相同的功能,尽管在这个特定的示例中,你需要一定程度的耐心。不过,我想说清楚的是,我个人并不反对Visual Studio或微软,我认为它是一个非常好的IDE,我每天都在使用它,并且对它很满意,我只是相信,Windows的调试工具在调试时给了您更多的控制和动力,当您没有安装Visual Studio(即生产服务器)的选项,或者您没有现成的源代码时,您就无法击败cdb和sos.dll来调试托管应用程序。
什么是异常?
让我们从头开始。异常是指在正常操作下,不应在任何时间发生但可能发生的事件。如果不进行处理,它会阻止应用程序运行。一个例外类似于你的车在你开车的时候停了下来。停车有很多可能的原因;你可能踩了刹车,发动机可能卡住了,车内的乘客可能拉了手刹,或者外部有人把一些相当坚固的东西挡在你的路上。不管发生了什么,如果你处理好了,有些事情你可以恢复,有些事情你不能。 异常要么是由CPU引发的;可能是零除或访问冲突。这是因为CPU有一套操作规则:如果它不喜欢这些参数,它会大叫并问你(开发人员)你想做什么。还包括一些软异常,如用户断点,因为这些将导致相同的结果。发生异常的第二种方式是应用程序要求Windows引发异常,然后由Windows发出请求,而不是CPU。要引发异常,应用程序使用win32函数raiseexception。如何处理异常?
为了围绕异常提供一个框架,Windows提供了结构化异常处理,CLR使用了结构化异常处理。发生的情况是,您使用SetUnhandledExceptionFilter注册一个处理程序,而Windows将按最近添加这些方法的顺序存储在链中注册的方法,依次访问每个方法,直到处理异常或终止应用程序。
.NET异常呢?
您可能想知道为什么,在.NET文章中,我已经描述了在C++ Win32应用程序中发生了什么。当然,这都是旧帽子,我们是在所有的时髦CLR世界?好吧,CLR是用c++编写的,当程序运行时,如果出现异常,它就是一个称为COMPlusFrameHandler的CLR函数。这在mscorwks.dll中。
如果您有rotor或sscli代码,并且想了解更多关于CLR如何处理异常的信息,可以在“sscli20\CLR\src\vm\i386\excepx86.cpp”中找到它,这可以做很多事情,但需要特别注意的是,它接受CPU异常(如访问冲突),并创建实际的.NET sysm.Access Violation exception或System.NullReferenceException(它将访问冲突转换为两者之一,请参阅sscli20\clr\src\vm\excepp.cpp中的MapWin32FaultToCOMPlusException)。COMPlusFrameHandler也为托管异常调用,因为MSIL代码调用RaiseException,然后导致调用结构化异常处理(SEH)。
无论是.NET异常还是CPU异常,CLR都使用JIT管理器来查找已引发异常类型的MSIL处理程序。如果找到处理程序,则在该点继续执行;如果没有,则检查SEH链以获取进一步的处理程序。处理异常或终止应用程序。
现在让我们看看内存转储文件来验证这一点。第一个例子不能用Visual Studio完成,所以它将在cdb中。
启动命令提示符,然后运行上一篇文章中的doDebug.cmd文件,然后执行“cdb-z c:\ pathToDumpFile\sampleCrash.dmp”。它应该打开转储文件并显示如下内容:这告诉我们它可以打开文件,并且它位于异常点(“这个转储文件中存储了一个感兴趣的异常”)。首先让我们看看SEH链:键入!exchain
这表明第一个处理程序是COMPlusFrameHandler,然后我们有许多其他的处理程序,最终以kernel32结束,如果它达到了这个程度,将终止程序。现在我们只需要理解,事实上,有一个CLR处理程序(CLR曾经被称为com+,原因我不知道)。理解COMPlusFrameHandler在内部使用CPFH_RealFirstPassHandler来实际处理异常也是很重要的。
我们现在应该加载sos.dll,也应该加载“.loadby sos mscorwks”。这只会将您返回到命令提示符,而不会出现任何错误。
让我们看看一些示例代码:
namespace SampleCrash { class Program { static void Main(string[] args) { throw new System.InvalidOperationException(); } } }
这引发了一个新的异常,那么我们为什么不看看MSIL是什么样子的呢?首先我们需要方法的描述,所以做“!Name2EE * SampleCrash.Program.Main
”。这显示了我们需要的细节:
如果使用sscli代码或反汇编JIT_THROW(sscli20\clr\src\vm\jithelpers.cpp或“uf JIT_THROW”),则可以看到它调用“raiseexceptioninternalonly”,然后调用“RaiseException”:
这显示了c#中的“throw new System.invalidooperationexception();”实际上调用了RaiseException,并显示了异常如何在SEH处理程序中结束。
CLR如何处理异常?
当从CPU异常调用CLR异常处理时,它需要创建.NET异常对象,因为托管catch块需要从System.exception派生的托管对象。CLR基于一个简单的switch语句创建一个特定的异常,该语句接受CPU异常并决定要引发哪些CLR异常。创建异常时,CPFH_FirstPassHandler会将其存储在托管线程的“throwable”属性中。然后再次调用CPFH_FirstPassHandler,此时因为“throwable”属性有一个异常对象,所以它会经过处理它的逻辑。当引发.NET异常时,已设置“throwable”属性,因此不需要随后创建异常。
调试时应该注意的一些事
在Visual Studio下调试托管进程时,始终将异常转换为.NET变量,即System.AccessViolationException,而不是cdb中的CPU异常代码,即C0000005,因此您可能认为在cdb下运行到Visual Studio的结果不同,但实际上是相同的。当您调试一个dmp文件时,因为您正在调试它,就好像它是一个非托管应用程序一样,所以它不会被翻译。
如果不处理异常会发生什么?
这取决于应用程序的类型,所有.NET应用程序在SEH链中的处理程序都比ComPlusFrameHandler高(如果要在cdb中看到链,前面提到的命令是“!exchain
“)。控制台程序显示异常的细节,然后退出。Windows应用程序和服务都将关闭,您可能会在事件日志中看到一条消息。
ASP.NET应用程序最有趣,因为通常未经处理的异常不会终止进程,而是会向web客户端写入一条消息,也就是死亡的黄色屏幕(这可以使用web.config关闭),消息被添加到事件日志中,连接被终止,但使用同一进程的所有其他web客户端都可以。某些异常(如堆栈溢出)可能会导致asp.NET工作进程崩溃,因此不要总是期望web应用继续运行。
因此,如果您发现您的应用程序或服务刚刚停止运行,那么您很有可能会遇到未处理的异常。在某些配置中的某些版本的Windows中,您可能会看到可信的“Dr Watson”进入生命,捕获内存转储,然后询问您是否要将其发送到Microsoft,以检查是否有已知的修复方法。如果您选择将转储排队到稍后(通过GUI或组策略),则可以从C:\ WINDOWS\pchealth\ERRORREP\QSIGNOFF获取它,其中可能有许多cab文件中包含转储文件。
内存转储
什么是内存转储?
内存转储是包含进程的全部或部分内存的文件。它们可能非常有用,也可能用途有限,这取决于它们是如何创建的,以及它们所包含的内存部分。
进程内存
让我们快速看一看进程是如何布局的,以及它如何使用内存来提供足够的细节,以便能够决定我们需要哪种类型的转储,以及为什么有时我们得不到所需的细节。启动进程时,从exe或dll文件读取二进制CPU指令并将其复制到内存中:然后,将字节的地址传递给CPU执行。这称为代码部分;我们需要它进行调试,以便能够知道当前的CPU命令是什么,以及它发生在哪里。如果我们有完整的符号,那么我们还可以找出源代码中当前命令的位置。
在这个过程中的某个时刻,我们得到了堆栈的空间。这些存储.NET值类型,并允许CPU和调试器跟踪我们的确切位置;我指的是指向当前位置的方法调用。如果我们不在转储中包含堆栈空间,那么我们就无法知道如何在特定位置结束以及传入了哪些参数。
最后一部分是堆。这就是创建.NET引用和大型对象的地方:没有这些,我们将知道我们拥有哪些类型的对象,但不知道这些类型的属性。
如何创建内存转储?
有一个名为MiniDumpWriteDump的API函数可以帮助创建内存转储,很多不同的应用程序都使用它来创建内存转储。在cdb中,这只是附加到进程,然后运行“.dump/ma c:.pathToDump.dmp”的情况。/ma开关会将所有堆栈空间、代码和堆添加到文件中。
其他工具是DrWtsn32(使用-p然后是进程id)和debug diag,它主要用于基于IIS的应用程序,但可以用于创建任何进程的转储。debug diag需要注意的一点是,它创建了两个文件,一个.dmp只是堆栈部分,另一个叫做.hdmp,它包含堆,因此它有更多的用途。