捕获ADPlus CLR崩溃
我想讨论一个我们都非常熟悉的场景。在过去的一年里你一直在拼命工作,在过去的几个月里,你甚至在晚上和周末工作。管理层对你的团队给予两周的休息,以感谢你的努力。但是现在你回到了办公室,你听到了来自你的技术支持部门的流言,说有些情况下你的应用程序因为神秘的原因在现场崩溃。你是做什么的?
应用程序恰好是用通过注册的未经处理的异常筛选器生成的AppDomain.UnhandledException事件。因此,您至少知道应用程序因InvalidCastException而失败,但是您无法想象为什么会发生这种情况。
如果您可以在受影响的系统上进行实时调试,那不是很好吗?除非您在现场为您的客户工作,或者您的软件在笔记本电脑上,并且您的客户愿意将其发送给您,否则我怀疑您是否会获得此机会。您需要的是一个工具来捕获应用程序失败时的状态。然后客户可以捕获这些信息并将其发送给您。
输入ADPlus。ADPlus是Debugging Tools for Windows包中的一个免费工具,它为CDB调试器编写脚本,允许您捕获系统上一个或多个进程的转储。它还具有以下优点:
- ADPlus可以监视桌面应用程序、服务等。
- ADPlus可以监视系统上的多个进程。当它收集这些进程的转储时,它会同时冻结和转储它们。这对于跟踪进程间通信的问题至关重要。
- ADPlus支持xcopy部署,这意味着客户不需要通过Windows安装程序等来安装任何东西。这将最大限度地减少机器上的配置更改,这对客户来说是一种音乐。
注意:尽管ADPlus可以xcopy安装,但您仍然必须通过Windows安装程序安装Windows调试工具包,因为这是微软发布它的唯一方式。但是,一旦安装了一次Windows调试工具,就可以xcopy将ADPlus或整个Windows调试工具包部署到另一台计算机上。事实上,在开发过程中,我发现将开发工具签入源存储库非常方便。Windows调试工具支持这一点,因为它是xcopy可安装的。
对于熟悉Windows Installer的用户,您可以使用msi执行管理安装,以便调试Windows工具,这将允许您提取文件,而无需在计算机上实际安装软件包,例如:msiexec /a dbg_x86_6.11.1.404.msi。
综上所述,让我们看看ADPlus如何帮助您诊断.NET应用程序的问题。
示例应用程序
我将引用我放在一起的C#3.0示例应用程序来演示如何使用ADPlus捕获.NET应用程序中未经处理的异常。代码如下:
using System; using System.Linq; using System.Runtime.Serialization; class A { public void SaySomething() { Console.WriteLine( "Yeah, Peter...." ); throw new BadDesignException(); } } class B : A { } class C { } class EntryPoint { static void Main() { DoSomething(); } static void DoSomething() { Func<int, object> generatorFunc = (x) => { if( x == 7 ) { return new C(); } else { return new B(); } }; var collection = from i in Enumerable.Range( 0, 10 ) select generatorFunc(i); // Let's iterate over each of the items in the collection // // ASSUMING THEY ARE ALL DERIVED FROM A !!!! foreach( var item in collection ) { A a = (A) item; try { a.SaySomething(); } catch( BadDesignException ) { // Swallow these here. The programmer chose to // use exceptions for normal control flow which // is *very* poor design. } } } } public class BadDesignException : Exception { public BadDesignException() { } public BadDesignException( string msg ) : base( msg ) { } public BadDesignException( string msg, Exception x ) : base( msg, x ) { } protected BadDesignException( SerializationInfo si, StreamingContext ctx ) :base( si, ctx ) { } }
您可以通过将示例代码放在一个文件中,例如test.cs,然后从Visual Studio命令提示符或Windows SDK命令Shell中执行以下命令:csc /debug+ test.cs
注意:代码是人为设计的,从设计/编码的角度来看,这段代码有很多不好的地方,但这是为了说明而故意的。例如,可能需要重新考虑引入一个包含对的引用的集合系统对象.
代码中值得注意的部分是EntryPoint.Main() 方法。在foreach循环中,我们在一个对象集合上迭代,我们假设这些对象都是从类型a派生的。因此代码尝试将所有实例强制转换为类型a的引用,因为我有意将C类型的实例放入该集合中,它将在某个时刻失败,但类型的异常除外System.InvalidCastException.
解决问题
在崩溃模式下使用ADPlus,我们可以捕获客户环境中的异常。为了说明这一点,让我们在崩溃模式下启动ADPlus并监视test.exe上面使用以下命令行构建的示例应用程序:adplus -crash -o c:\temp\test -FullOnFirst -sc c:\temp\test\test.exe
注意:在我写这篇文章的时候,我在我的机器上的一个名为c:\temp\test的目录中构建并测试了这段代码。因此,您将在本文中看到对它的引用。顺便说一句,如果我找到了很多适合你的路径。如果您遇到了神秘的错误或行为,并且您正在使用ADPlus的相对路径,请尝试在您的头撞在墙上太长时间之前尝试完全限定这些路径,以找出可能出错的地方。
如果将Windows调试工具的安装目录添加到PATH环境变量中,则ADPlus更易于启动。上面的命令行假设这已经完成。现在,让我解释一下我上面使用的命令行选项。我强烈建议您熟悉所有ADPlus命令行选项。
- ·-crash在崩溃模式下启动ADPlus。如果应用程序因未经处理的异常而失败,则可以使用此模式。
- ·-o c:\temp\test告诉ADPlus我希望将输出放在c:\temp\test中,这是我构建测试.exe应用程序。
- ·-FullOnFirst对于托管应用程序非常重要。。这将告诉ADPlus在第一次出现异常时获取一个完整的进程转储。必须为托管应用程序捕获完整的转储,否则,有关执行引擎和托管堆的所有必要数据都将从转储中消失,从而无法有效地进行调试。
- ·-sc c:\温度\测试.exe只是将ADPlus指向要监视的应用程序的方法之一。在本例中,我们指示ADPlus告诉调试器显式启动应用程序。如果它是一个服务,或者如果您要监视的应用程序已经在运行,我们可能会使用-p附加到它的PID,或者使用-pn按名称附加到进程。请注意,我提供了应用程序的完整路径。
启动ADPlus后,除非使用–quiet选项,否则将显示以下对话框。
一旦应用程序完成执行,转到在ADPlus–o命令行选项中指定的目录,您应该会看到一个子目录,其名称与您在上一个对话框快照中看到的类似。例如test.exe刚执行,那个目录名为Crash_umode_uDate_05-11-2009_uutime_21-12-54PM。在该目录下,我列出了许多文件:
C:\temp\test\Crash_Mode__Date_05-11-2009__Time_21-12-54PM>dir /b ADPlus_report.txt CDBScripts PID-0__Spawned0__1st_chance_CPlusPlusEH__full_15ac_2009-05-11_21-13-01-332_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-55-482_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-56-527_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-57-370_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-58-103_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-58-867_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-12-59-663_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-13-00-505_0eac.dmp PID-0__Spawned0__1st_chance_NET_CLR__full_15ac_2009-05-11_21-13-02-689_0eac.dmp PID-0__Spawned0__1st_chance_Process_Shut_Down__full_15ac_2009-05-11_21-13-27-743_0eac.dmp PID-0__Spawned0__2nd_chance_NET_CLR__full_15ac_2009-05-11_21-13-04-140_0eac.dmp PID-0__Spawned0__Date_05-11-2009__Time_21-12-54PM.log Process_List.txt
对于每个第一次出现的异常,都会收集一个转储文件(.dmp)。请注意,由于–FullOnFirst选项,转储文件可能会非常大。您可以随后将这些转储文件加载到Windbg(或其变体)或visualstudio调试器中。就个人而言,我更喜欢Windbg,因为我可以加载SOS扩展和SOSEX扩展,并深入了解应用程序和CLR的状态。
使用上面的ADPLUS默认配置,您可以看到ADPUS生成第一个机会C++异常、八个第一机会CLR异常、一秒钟机会CLR异常和在进程关闭期间收集的一个转储的转储。
输出中包括什么
Process_list.txt执行时生成列表.exe,另一个与Windows调试工具一起提供的工具。
最后,CDBScripts子目录包含一个.cfg文件,这个文件就是ADPlus生成的调试器脚本,随后被输入到CDB以完成任务。在我的机器上,当使用上一节中的命令行对示例应用程序运行时,此文件名为PID‑0_uuSpawned0.cfg。如果您想知道ADPlus究竟指示调试器做什么,那么这个文件就是源文件。
注意:名称包含零PID的原因是因为我们使用了–sc选项来启动应用程序。如果我们使用了–p或–pn选项,文件名中的PID将不为零。
我不建议在实时调试器中执行此调试器脚本,因为这样做可能会覆盖已收集的数据。相反,如果您需要将ADPlus与实时调试相结合,那么应该使用–gs选项,我稍后将介绍这个选项。
确定特定的异常
您会注意到,在ADPlus的上一次运行中,生成了很多转储文件,每个转储文件都相当大。在现实中,有时第一次机会的异常并不总是意味着致命的情况。在Accelerated C#2008中,我详细介绍了使用异常实现任何类型的控制流或其他合理预期行为是多么糟糕的设计实践。这是因为异常应该是真正的异常事件!关于异常有多昂贵的更多有趣的细节,我邀请您阅读Tess Ferrandez关于这个主题的一篇优秀的博客文章。无论如何,您可能会遇到这样的情况:您可能会遇到这样的情况:对于您不感兴趣的第一次机会异常,您会得到大量转储,如本例所示。。
为了缓解这种情况,您可以创建一个ADPlus配置文件来耦合!SOS扩展提供的StopOnException命令,指示ADPlus只过滤出您感兴趣的异常。为此,我创建了一个名为过filter.config包括以下内容:
<ADPlus> <!-- Configuring ADPlus to log only exceptions we're interested in --> <Exceptions> <Config> <!-- This is for the CLR exception --> <Code> clr </Code> <Actions1> Log </Actions1> <CustomActions1> .loadby sos mscorwks; !StopOnException System.InvalidCastException 1; j ($t1 = 1) '.dump /ma /u c:\dumps\InvalidCastException.dmp; gn' ; 'gn' </CustomActions1> <ReturnAction1> VOID </ReturnAction1> <Actions2> Log </Actions2> </Config> </Exceptions> </ADPlus>
<CustomActions1>元素是此配置文件中感兴趣的元素。此元素允许您指定调试器应在第一次出现异常时执行哪些命令。在这个元素中,您可以放置任何有效的调试器命令(除了与windbg GUI相关的命令)。如果您需要执行多个命令,如我前面所述,只需用分号分隔它们。在上面显示的<CustomActions1>元素中,我首先使用.loadby命令加载SOS扩展。然后,我用谓词模式下的!StopOnException命令,如果异常类型为,则将$t1伪寄存器设置为1,System.InvalidCastException否则为0。然后,如果$t1为1,则使用下面的j命令创建一个完整转储,否则不执行任何操作。j命令的两个路径中的gn命令告诉调试器不进行处理,这样异常就会向上传播,而不是被调试器吞并。如果要处理异常,从而吞没异常,代码中的异常处理程序将永远看不到它,调试器将改变应用程序的行为。最后,请注意,用于创建转储的.dump命令指示存储转储的路径。我把它放在我的电脑目录里。
现在可以使用以下命令将此配置文件提供给ADPlus: adplus -crash -o c:\temp\test -FullOnFirst -c c:\temp\test\filter.config -sc c:\temp\test\test.exe
收集的垃圾较少。除了SudioCudiExtExchange的转储,它还捕获C++异常以及应用程序关闭时。如果在调试器中打开C++异常转储并检查堆栈,则表明CLR正在生成无效的异常。CLR捕获C++异常并将其转换为托管异常。因为我在前一条命令行中保留了-FulLoMax选项,所以生成了C++异常转储。您可以通过移除FullOnFirst来消除C++异常转储。
在实时调试期间使用ADPlus
我已经在本文的前面提到了–gs ADPlus命令行选项。此选项允许您创建ADPlus创建的调试器脚本,而无需在调试器中实际运行它们。例如,如果执行以下命令(我从c:\temp\test目录执行了我的命令):adplus -crash -o c:\temp\test -FullOnFirst -c c:\temp\test\filter.config -gs livedebug
您会注意到ADPlus实际上并没有启动任何调试器或应用程序。相反,它创建了一个名为livedebug的子目录。当您进入该目录时,您会注意到它的布局与前面演示中创建的崩溃目录类似。在我的机器上,我得到了以下两个文件:
C:\temp\test\livedebug\ADPlus_report.txt
C:\temp\test\livedebug\CDBScripts\PID-0__livedebug.cfg
PID-0__livedebug.cfg文件实际上是一个包含调试器命令的调试器脚本文件。我们现在要做的就是在调试器中启动测试应用程序,然后执行这个脚本。在我的c:\temp\test目录中,我可以使用以下命令启动调试器:windbg test.exe
进入调试器后,我可以通过执行以下 $$<命令 来调用ADPlus调试器脚本:$$<C:\temp\test\livedebug\CDBScripts\PID-0__livedebug.cfg
执行$$<命令后,您将注意到,该脚本将接管并执行与ADPlus从命令行按常规方式运行时相同的操作。
作为进一步的实验,编辑过滤器.config文件并删除gn命令。现在,调试器将在执行自定义命令后等待用户输入,而不是继续执行应用程序。如果您希望在ADPlus遇到特定情况时有机会手动执行调试,这可能会很方便。
第一次机会vs.第二次机会例外
在这篇文章中,为了说明问题,我一直在关注第一次机会的例外情况。但是,很多时候,您只对真正未处理的异常感兴趣。在这种情况下,您只想捕获二次机会异常。如果您需要捕获抛出异常的确切时间点的状态,并且在操作系统搜索任何处理程序之前,您肯定希望捕获第一次出现的异常。此外,如果您怀疑正在调试的应用程序可能处理异常不适当(甚至可能盲目地吞并它),那么您肯定希望在这种情况下捕捉第一次的异常。
结论
在这篇博文中,我向您介绍了ADPlus以及它在解决现场问题时提供的实用程序。ADPlus能够很好地捕捉现场的异常情况,因为它不需要在受影响的机器上进行配置更改,从而使它成为您的客户容易吞下的药丸。在开发阶段与质量保证团队合作时,您可能会发现这非常有用。例如,您有多少次遇到这样的情况:问题只出现在一个随机的QA工程师办公室后角的一台积满灰尘、很少使用的机器上?有多少次,在这种情况下,您觉得有效解决问题的唯一方法是安装visualstudio调试器并开始在该计算机上工作?此外,如果问题只发生在那台满是灰尘的旧机器上,大约一周一次呢。ADPlus可以帮助您避免这种疯狂,它提供了一种简单的机制来捕获有问题的机器上的完整进程转储,这样您就可以将这些转储放到您信任的开发机器上进行进一步的调试和分析。