=================================================
本文为He,YuanHui(khler)翻译,本文可任意转载
但是转载时必须确保本文完整
并完整保留原作者及译者信息以及本文原始链接
E-mail: khler@163.com
QQ: 23381103
MSN: pragmac@hotmail.com
英文/原文地址:http://blogs.msdn.com/kirillosenkov/archive/2008/12/07/how-to-debug-crashes-and-hangs.aspx
=================================================
在我的C# IDE QA工作中,我学到了一些关于Visual Studio的非常有用的调试方法,我愿意在这里与大家分享,希望对你们有所帮助。尽管截图来自于Visual Studio 2008 SP1,但也基本适用于其他的VS版本。
丰富的调试支持
当你点击F5调试你的C#程序时,目标进程(你的程序)被执行,然后,在你的代码执行期间,Visual Studio 进程将调试器挂接到你的进程上,这样,你就可以通过断点进入到调试器中,VS将为你提供各种类型的调试支持:高亮显示当前语句、调用堆栈、(变亮)观察、定位、立即窗口(用于在当前上下文中执行一些命令、操作,非常有用——译者注)、运行时编辑,等等。
更重要的是,如果你的程序抛出了异常或者崩溃,调试器就会拦截异常或崩溃,为你显示所有异常信息。
另外,Visual Studio 也提供了一种不附加调试器而直接执行你的程序的的方法:Ctrl+F5,你可以在你的程序中抛出一个异常,来体验一下F5与Ctrl+F5的不同。
throw null
对我而言,我更喜欢通过throw null来手工抛出异常,原因在于它刚好做了我希望做到的事:由于调试器找不到异常对象,它抛出了NullReferenceException异常。
崩溃和Watson对话框
在没有代码的情况下,不是通过F5启动的进程或者系统装载的进程崩溃了或者抛出了异常,你该怎么办?我记得在我进入微软之前,我唯一能做的事是发泄我的不满(通常是用俄语)。而现在,我不再这么无助,因为我学会了一些技巧,当程序崩溃时,我可以启动VS并通过VS来调试它。
怎样用Visual Studio调试崩溃
最近Viacheslav Ivanov 报告了我们c#语言服务包中的一个有趣bug(Visual Studio程序自身的一个bug,作者下面会把Visual Studio当作一个普通进程来利用另一个Visual Studio实例对它进行调试——译者注)。在Visual Studio中,保存好你的工作,将下面的代码拷贝到一个C#控制台应用程序中,然后将其中的 ’object’ 改成 ’int’ :
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
你将看到Watson对话框:
我们得到了一个调试VS的机会,但是我建议你点击"发送错误报告",许多人"不发送",坦率的说我不能理解为什么不。里面没有个人信息,并且无论如何我们也不需要个人信息。我们想要的只是调用堆栈和内存堆栈信息。如果你能把错误报告发送给我们,将对我们有非常大的帮助,我们可以利用这些信息修正Visual Studio,使它变得更稳定。顺便说一下,我们通常优先修正那些从这个对话框得到的反馈最多的bug,所以,如果你也报告了错误,我们修正你碰到的这个bug的机会就会更多。例如,我们已经修正了上面对话框显示的bug,当前版本在处理这种事务时工作得很好。
挂接调试器
那么如果应用程序崩溃或者挂起了,你能做什么呢?你可挂接一个调试器到正在运行的进程上,尽管进程已经崩溃,代码仍在被执行(通常情况下,崩溃的应用程序的主线程还在继续为错误对话框分派消息)。你也可以在Watson对话框中选择“调试”,或者(像我一样)启动一个Visual Studio新实例并手动附加到此进程,而不会丢失Watson对话框。
注:如果被调试进程已经崩溃,你将不得不通过暂停按钮来手动中断进程以便进入调试状态, 如果调试器已经附加到进程,你可以选择中断或者继续。
你可以通过Tools | Attach To Process (Ctrl+Alt+P)将Visual Studio 调试器附加到一个执行中的进程:
你将看到一个附加进程对话框:
注意,这里有个有趣的事:Attach to: selection。由于Visual Studio是一个混合模式的托管/内嵌应用程序,要得到托管的或者内嵌的调用堆栈,你应该通过selection为他们都附加一个调试:
如果你选择了Managed和Native,你将得到丰富的调用堆栈信息——这是我所推荐的。
注意:如果你想激活混合调试模式(Managed+Native)调试带有源代码的应用程序,你可以到启动工程(如果一个解决方案中包含了多个工程,那么要指定一个引导工程作为启动工程,用于启动程序——译者注)的工程属性页面,在Debugging标签中选择"Enable unmanaged code debugging"。然后,调试器将自动使用混合模式附加到进程。
最后,从进程列表中选择你感兴趣的进程(在我们的例子中为devenv.exe),然后点击Attach。注意,调试器自己的进程将不会显示在列表中,这就是为什么你没有在进程列表中看到两个devenv.exe的原因。
远程调试
很多人不知道,VS也可以调试运行于网络中的另一台机器上的进程。要想这样做,你需要先在你要调试的进程的机器中开启Visual Studio Remote Debugging Monitor:
远程调试监视器将监视另一台机器的调试器的连接,这样,你就可以通过Attach对话框利用Transportation and Qualifiers将调试器附加到远程进程中:
你可以到MSDN中找到更详细的关于远程调试的信息。
将Enable Just My Code设置为false
在Visual Studio中,一项非常重要的设置就是"Enable Just My Code",默认情况下它被设置为true。要想在调用堆栈中看到更多信息,你需要到Tools | Options下禁用该项:
我通常在安装后就立即把它禁用了,以便我总是能在"not my code"的情况下调试。
在这个设置页面中,另外几项也很有用:
· Enable .NET Framework source stepping – 如果你想进入.NET framework源代码中调试,把它设置为true
· Enable Source Server support (作者另写了一篇文章专门用于讲解此项设置——译者注)
· Step over properties and operators (Managed only) – 如果不想进入属性的读取器调试,禁用此项——只有打了SP1后才有
· Require source files to exactly match the original version – 用于没有与.pdb符号文件精确匹配的源代码而是相近的版本文件的情况
在第一次异常时就中断
在所有的Debug设置中,一项非常有用的设置就是无论如何都在第一次异常(first-chance exception)被抛出时就中断。” first-chance exception” 是一个可能被程序自己在后续的或者附近的异常捕获块区中捕获的异常。 ”first-chance exception” 是一个对用户来说非致命的或可忽略的异常,所以在程序运行期间它们甚至不会被最终用户看到。然而,如果一个 ”first-chance exception” 异常没有在代码中的得到处理,就有可能像水泡一样上升,最终进入到CLR或者OS中,从而导致崩溃。
所以,要想在” first-chance exception”时中断,你可以到Debug | Exceptions中进行设置,以触发异常对话框:
在这里,你可以将Common Language Runtime Exceptions勾选,以便托管异常每次被抛出时进入调试。通过这种方式,你可以看到很多隐含的异常,有些甚至来自于深藏不露的.NET Framework类库中。有时异常实在太多了,以至于你无法忍受,但是它们确实对你发现问题根源非常有帮助,因为它们也为你显示了问题的来龙去脉;另一个在 ”first-chance exceptions” 时中断的好处是此时的调用堆栈是非常简单的,而不是经过了层层调用和包装的,而且问题帧任然在堆栈中。
调试工具窗口
好了,现在我们知道了怎样为进程附加调试和如何设置调试选项。现在让我们看看在附加了调试器后都发生了些什么。在我们的练习中,你可以打开两个Visual Studio实例,将第二个实例附加到第一个上作为调试器来调试第一个实例。然后用上面的拉姆达表达式代码使第一个进程崩溃。替代Watson对话框,这次你将看到下面的调试窗口:
现在,你得到了一个中断程序并发现哪儿出了问题的机会。如果在 ”first-chance exception” 异常发生时想让用户代码去处理,”Continue” 是一个非常有用的操作。让我们点击 ”Break” 看看在调试状态下能看到什么工具窗口。
进程窗口
实际上,通过Debug | Windows,你可以看到所有可用的调试窗口。进程窗口(Processes window)显示了当前调试器附加到的所有进程的一个列表。一个很不错的小技巧是:实际上你可以同时为多个进程附加调试器。然后,你可以通过进程窗口来切换“当前”要调试的进程,从而更新所有其他调试窗口的信息以显示“当前”调试进程的信息。
注:在我们的小组中,我们会很频繁的使用这个窗口,原因是我们的所有测试是“在进程外”执行的。我们的测试进程是在一个独立的进程中启动Visual Studio 的,然后利用DTE、远程或其他技术进行监视。一旦有失败发生,为测试进程和Visual Studio进程附加调试器是非常有用的,尤其是调试一个被附加到另一个产生崩溃的进程的调试器,会产生非常有趣的问题:如果调试器在调试另一个调试器时在同一个bug处崩溃,你就会有麻烦了;),我应该在我的blog中多写些我们每天工作中发生的这些有趣的故事,但是,我似乎要跑题了。
线程
我们都知道,进程一般都有多个线程。如果你进入一个被调试的进程,你可能最希望得到一个与当前进程相关并且在中断时刻处于活动状态的线程列表。主线程是一个在分类列中被标注为绿色的线程,它是你的应用程序(在我们的示例中就是Visual Studio)的UI线程。主线程执行应用程序的消息循环,为应用程序的每个窗口分发消息。如果一个消息框或者对话框被显示,那么你就能在主线程中看到这个对话框的消息循环。
要切换线程,只要双击你感兴趣的线程名就可以了。多数情况下,你仅仅对主线程比较感兴趣,但是如果你启动了你自己的线程,那么最好给它一个名称,以便在上述列表中很轻易的找到。
调用堆栈
每个线程有一个调用堆栈。调用堆栈也许是用来发现崩溃所在的最重要的手段了——是怎样的函数调用过程最中导致了崩溃的发生?如果程序被挂起了,你可能更想知道是在哪个函数中被挂起并且是如何走到了这个函数的,你只需要浏览一下调用堆栈就会很快明白到底发生了什么。“这个调用过程理应如此吗?”或者“这个调用过程中都有谁”也许是我们在调试中问的最多的问题。
不管怎样,我们先看看下面的堆栈窗口:
在我们的例子中,我们在Callstack中看到了C#语言服务模块(在调试的世界里,*.dll和*.exe都被称为模块)。
然而,从cslangsvc.dll中,我们看到的是函数在内存中的地址,而不是过程函数名,这是因为cslangsvc.dll模块的符号文件没有被加载。下面很快我们将看到这个符号文件是怎么样被加载的。
模块
模块对话框显示了一个被调试的进程装载的所有.dll和.exe模块的列表:
有很多为给定模块装载符号的方法,你可以在模块上右键点击来得到一些操作列表:
每个模块的符号都存储在.pdb文件中,这个文件在每次创建调试版的二进制程序时生成。.pdb文件保存了从编译好的二进制文件映射到源代码的所有信息,以便调试器在调试二进制模块时能够显示丰富的调试信息(函数名、代码行位置等等)。如果没有调试符号,你只可能处于汇编层和寄存器窗口模式,你没法映射到源码层进行调试。
符号
一个非常有用的对话框就是Tools | Options | Debugging | Symbols:
在这里,你可以设置查找.pdb文件的路径。通常情况下,.pdb文件将被直接与二进制模块放在一起,紧随其后,以便自动查找和装载。装载符号文件一般需要消耗一些时间,所以Visual Studio 支持将符号文件缓存到某个目录下。而且,如果你不想为所有装载的二进制模块装载符号(这通常要消耗很多时间),你可以将"Search the above locations only when symbols are loaded manually"勾选。你可从提供微软产品(如Visual Studio)符号支持的微软符号服务器中装载符号,你也可以从模块或调用堆栈窗口、通过右键点击模块名并选择Load Symbols From选择装在位置。由于我们要调试Visual Studio,它的符号可以从微软服务器上加载:
在我们装载公用符号时,将出现下面的对话框:
在我们装载了cslangsvc.dll后,我们发现在Call Stack窗口中出现了更多的调试信息:
得到了这个调用堆栈,任何一个程序员都可以很轻松的精确定位问题所在。对于我们的实例,我们可以看到在我们试图为GenerateMethod反射显示Smart Tag时出现了问题:
就像你看到的,这里仍然还有很多没有为其装载符号的模块。你可以通过右键为其装载符号。为了看到更多更细的信息,我们推荐你装载所有符号。
这就是为什么在报告bug时调用堆栈如此重要的原因所在。要保存call stack,你可以通过快捷键Ctrl+A和Ctrl+C拷贝所有堆栈信息到剪切板中,当你在Watson对话框中点击Send Error Report时,调用堆栈信息就被包含到了错误报告中了。
你也能看到,如果没有符号,调用堆栈实际上不是很有用——只有“符号+调用堆栈”才提供了比较丰富的有关崩溃的消息。
Minidump
提供崩溃进程的有用信息的另一个非常有用的信息是内存dump(heap dump、minidump)——这实际上就是进程崩溃时的内存快照。要创建一个minidump,进入Debug | Save Dump As,Visual Studio 将提供一个存储dump到本地硬盘的机会,你可以选择存储”Minidump”或者”Minidump with Heap”:
”Minidump with Heap”将保存比”Minidump”更多的信息,但是要占用更多磁盘空间(对于Visual Studio,有可能有几百兆——取决于进程崩溃时的工作集大小)。
在报告崩溃时,通常有三部分发送到了我们的开发人员:
1. Call stack
2. Symbols
3. Minidump with heap
大多情况下,他们能够理解你给的这些信息的问题所在。附上一个产生bug的过程列表也许是非常有用的,因为他们可以自己重现这些问题并激活first-chance exceptions来尽早进入问题域。
调试挂起
如果一个程序被挂起了,你可以为其附加一个调试,然后点击 ”Break” 来看看是什么线程和什么调用堆栈正在运行。通常调用堆栈能给你一些进程挂起的线索。调试挂起与调试异常没有什么两样。
微软愿与客户共担痛苦
最后,我推荐大家看看这个视频:(我们真的感受到了客户的痛苦了吗?)
总结
在这篇文章中,我探讨了这样一些对调试比较有价值的东西:
· 关闭 "Enable just my code"
· 利用Tools | Attach To Process 来附加调试进程
· 为混合模式选择Managed, Native,激活非托管代码调试
· 将调试器附加到多个进程
· 选择进程和线程
· 通过在Debug | Exceptions中激活first-chance exceptions,在第一次异常时调试程序
· 怎样选择正确的线程
· 怎样装在调试符
· 怎样查看 call stack
· 怎样保存 minidump 文件
如果你有什么想法或文中的错误纠正,请联系我。