查看.NET应用程序中的异常(下)
为什么要使用内存转储进行调试?
在两种主要情况下,您可能需要使用内存转储进行调试。第一种情况是应用程序有一个未处理的异常并崩溃,而您只有一个内存转储。第二种情况是,在生产环境中出现异常或特定行为,并且在排除故障时不能将调试器保留在附件中,因为调试器可能会中断用户服务。相反,您可以附加cdb,在正确的位置创建转储文件,然后分离调试器,这意味着应用程序只在服务中有一个小的中断时继续运行。当然,取决于你的环境,这并不总是可能的,但有时可能是你唯一的选择。让我们来看看如何在cdb和Visual Studio中使用内存转储进行调试。这来自本文附带的example2.exe应用程序。它有很多选项,但我们将要讨论的是为什么cdb比Visual Studio更快。首先,您应该启动一个命令提示符并使用调试工具切换到目录,然后运行“doDebug.cmd”文件,如前一篇文章所述(也附在zip文件中)。然后是“cdb-z c:\pathToDumpFile\stackOverflow.dmp”。应按照以下要求打开:
这表明我们有一个内存充足的小型转储,所以我们有我们需要的部分。我们有一个异常,在这种情况下是“堆栈溢出”异常。 我们知道存在堆栈溢出,但不知道这是托管还是非托管问题,所以让我们获取本机堆栈跟踪“kf”
这显示了mscorwks!CallDescrWorker调用了未知的方法。未知的原因是调试器使用模块列表来确定模块的名称(从“lm”获取模块列表),因为CLR JIT的MSIL进入程序集,所以当前模块中存在,因为它挂在进程中的某个位置。
因为mscorwks调用了未知函数,所以这很可能是一个托管问题。在上面的堆栈跟踪中,我使用了kf,因为这些显示了每个堆栈帧使用了多少内存。它实际上计算出了两帧之间的距离,因此第一帧没有内存使用。内存之所以重要,是因为有两种类型的堆栈溢出:第一种是在堆栈上创建太多或太大的对象;第二种是函数递归并耗尽所有内存。
那我们在哪?我们知道这可能是托管代码问题,所以让我们加载so s.dll,'loadby sos mscorwks'并获取堆栈跟踪。注意,因为这是递归的问题,如果你做了!CLRStack“调试器将在cdb中显示一帧又一帧。从Ctrl+C停止它,它将中断跟踪。使用cdb的一个技巧是,如果你需要做一些能输出大量数据的事情,那么就把窗口降到最低,这样就可以让整个团队受益。
所以我们现在有一个显示导致堆栈溢出的函数名的堆栈跟踪:如果我们有源代码,我们可以检查它并查看可能发生的情况,但是让我们快速查看一下MSIL。 和往常一样,我们需要获得helper方法的方法描述;所以我们通过执行“!Name2EE * example2.StackOverflow
“。然后我们从“MethodTable:”中获取值并执行“!DumpMT -md 019631a4“。
那么,要得到MSIL,就做“!DumpIL 001963190“我们得到:
所以这很有趣,“ldarg.1”抓取第一个参数,“ldc.i4.1”抓取值1,“add”将它们相加;所以它递增某种类型的计数器,然后它根据_max_RecurseLevel检查结果,如果它大于或等于它,则它退出函数(“bge.s IL_0016”)。这是一个相当简单的例子,但是现在我们需要做的只是看看maxRecurseLevel和传入的值,看看我们在哪里。
顺便说一句,这些是msil文档的链接,当您不了解msil正在做什么时,使用msdn查看每个操作员的操作相当简单:
Ldarg.1 http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldarg_1.aspx
Ldc.i4.1 http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldc_i4_1.aspx
Bge_s http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.bge_s.aspx
Add http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.add.aspx
注意“this”指针是如何作为参数传递的,它总是第一个,也注意我们只在top方法上看到它。这是因为CLR优化了代码,而不是在堆栈上传递引用,这样会占用更多的空间,而是将其存储在寄存器中并使用它。这是一个CPU级别的优化,这意味着您的应用程序更快,但调试有时可能会更困难。
现在让我们通过“!do 0x01ff66a0“来dump出"this"的值
这表明maxRecurseLevel是100万,可能太高了,所以我们知道需要做什么来修复它。如果我们想猜测一个精确的数字来使用,那么我们可以看到另一个参数的当前值,我们知道这个参数每次都会增加一个,以查看我们有多少个参数。第一个堆栈帧上的值是“i=0x0001f88c”,所以我们只需要将这个十六进制数转换成可读的值?0x0001f88c
所以我们知道它会爆炸大约13万个递归调用。如果我们想知道它是如何进入递归函数的呢?好吧,我们别无选择,只能跑一次“!CLRStack“然后让它完成,我建议添加-a,这样你就可以得到所有的参数,并且在完成之后最小化窗口(记住它有129164个帧要转储,在它到达剩下的代码之前!)如果你想跑“!CLRStack-a“完成后,需要一两分钟,您应该得到:
这向我们展示了Main方法称为helper——如果您查看源代码的话,这并不是严格意义上的正确方法。因为它是一个发布版本,所以很多代码得到了优化,许多参数的值也得到了优化。若要查看堆栈上随时可用并存储的所有值,或在堆栈上具有引用,请执行“!dso”。使用所有这些信息,我们应该有足够的信息来跟踪并修复错误。
正如所承诺的,我说过我会尽可能在Visual Studio 2008中介绍这个示例。如果您启动Visual Studio,请转到文件打开解决方案并浏览到dmp文件。加载后,转到解决方案资源管理器并右键单击转储文件。然后选择“调试步骤进入新实例”。当这开始时,系统会提示您一个“堆栈溢出”异常,这样我们就知道出了什么问题。如果你点击“中断”,你可以进入调试器。
需要注意的是,调用堆栈是错误的,它只显示了一个显然不对的函数,它显示了一页反汇编,但是很难看到您的位置。要查找当前行,您需要显示registers窗口,并查看eip的值,该值给出了您当前所在的地址。。
因此,我们无法获取本机堆栈跟踪来查看我们的位置。相反,让我们继续加载sos.dll并获得“!CLRStack“。如果在打开时打开命令窗口(查看其他窗口命令窗口),如果有“>”提示,则键入“immed”进入立即模式。
如果您输入“.load sos”–我必须承认,在Visual Studio中加载sos.dll的语法确实比在cdb中更容易。这应该表明dll已经加载,现在就做“!CLRStack-a“:但要注意,它需要很长时间才能完成,而且在它完成时,除非您杀死Visual Studio,否则您将无法使用它或停止它。我总是在大约20分钟后杀死Visual Studio,如果你有一台更快的机器或更多的耐心,那么它很可能完成。
当它完成后,你可以继续从我们在cdb得到的痕迹,所以“!Name2EE*example2.StackOverflow”并从“MethodTable:”和“do”中获取值!DumpMT-md 019631a4“然后给你方法描述,然后你可以运行”!扔垃圾“反对。
显然,这是一个有利于cdb的演示,但是Visual Studio的速度确实较慢,而且还有一些时候它明显是这样。另一个我将在以后的文章中进一步讨论的是“奇妙的命令”!DumpHeap-stat“–这是我们用来跟踪内存泄漏的,因为它显示了所有不同类型的对象的列表以及它们使用的内存量,所以您可以一眼看到内存的确切去向。我们开始吧!cdb中的DumpHeap-stat“
这向我们展示了通过增加内存使用量来排列的所有对象,因此列表越往下,该类型使用的内存就越多。在这个视图中,我们得到方法表、计数和总大小。我之所以强调example2.StackOverflow,是因为它是另一种简单的获取类详细信息的方法,我们想进一步研究这个类,在这种情况下,我们有一个它的实例,所以它很简单,如果它是System.String,或者是一个经常使用的类,那么在跟踪正确的类时需要做更多的工作。找到类的具体实例就行了!DumpHeap-type example2“(或-type example2.StackOverflow,因为它是一个子字符串匹配项,两者都可以工作):
如果是的话!DumpHeap-type System.String或其他类,然后,如前所述,列出的将远远不止一个。如果我们想看的对象,我们只是简单地做“!do 01ff66a0“,这是对象的地址,我们可以再次看到maxRecurseLevel的值:
总结
我在本文中已经讨论了相当多的主题,并希望解释了CLR如何在非托管运行时上下文中处理异常。理解它是如何工作的很重要,因为在您自己的代码中使用异常是.NET编程的一个基本部分;理解它的工作方式将有助于在cdb中调试问题,甚至没有.NET异常。
如果您可以从客户或实时生产服务器获取内存转储,那么这是一种解决问题的简单方法,因为您可以很容易地看到崩溃或其他事件的原因。如果您想要一个如何使用它们的示例,Microsoft有一个服务,当您遇到崩溃时,它可以建议使用知识库或支持页来帮助解决问题。watson博士(dw20)可以获取进程的内存转储,然后将此转储上载到其服务器;然后根据已知问题分析堆栈,以自动将您重定向到知识库或支持页以帮助解决问题。微软审查了每种产品中最常见的崩溃,这是他们修复实际影响用户的问题的一个非常好的方法。如果您自己提供软件,那么我建议您在产品中添加一些内容,以便在必要时获得用户内存转储。
至于cdb/Visual Studio,我相信如果你花时间学习Windows的调试工具是如何工作的,你会发现这项工作是值得的。我不能保证你需要经常使用这些技能,但我保证,在某些时候,它们将是无价的。