查看.NET应用程序中的异常(上)

内存转储是查明托管.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”。这显示了我们需要的细节:

Module: 00752f2c (SampleCrash.exe)
Token: 0x06000001
MethodDesc: 007532c0
Name: SampleCrash.Program.Main(System.String[])
JITTED Code Address: 007f0070
 
现在让我们看看MSIL代码“DumpIL 007532c0”
0:000> !DumpIL 007532c0
ilAddr = 00802050
IL_0000: nop
IL_0001: newobj System.InvalidOperationException::.ctor
IL_0006: throw
 
这将创建一个新的invalidooperationexception并调用MSIL函数“throw”–现在让我们通过获取“JITTED code address”(007f0070)并运行“,来查看此操作的程序集代码!U 007f0070英寸。这里我们看到调用invalidooperationexception构造函数,然后调用“JIT_Throw”

 

如果使用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,它包含堆,因此它有更多的用途。

posted on 2019-11-11 16:59  活着的虫子  阅读(644)  评论(0编辑  收藏  举报

导航