Windows平台调试技术(一)
一、简介
本文简要介绍了Windows程序的调试技术。范围限于用户模式,包括了基本调试工具的使用:WinDbg,procdump。
二、背景
当使用Windows程序时,我们时常由于未知的原因停止了工作。其现象通常是一个对话框,像下面这样:
当我们看到这种情况时,我们通常选择“关闭程序”然后重新启动程序。如果还是一样的结果,此程序又是第三方提供的,那么我们就会报告这个问题然后等待回复。
现在,我们转换角色,站在开发此软件的团队来看待这个问题、尽快给出一个解决方案。我们进入细节,一步一步分析此应用程序停止的原因,以及该如何解决。
三、定义
应用程序崩溃是一种停止程序运行的异常。看看下面的代码例子:int main() { int *p = NULL; cout<<"This is Start"; *p = 10; cout<<"This is End"; return 0; }
当我们运行此代码编译出来的exe文件时,就会看到像上面的崩溃窗口。出现此情况的原因是“*p=42”,给未分配内存的指针赋值,或者说给空指针赋值。我们能找出此问题的原因是因为此源码不多,容易分析。假定此情况出现在成千上万行代码中,修复此问题将会十分困难。因此我们需要一些技术来精确的获取出现此问题的源代码(或者相关原因),而不是分析整个代码。
四、调试技术
有很多不同的技术可以找出程序崩溃的原因,但是不同的技术中很多都是类似的。
第一步:找出出错模块
可以使用事件查看器来找出出错模块。以我们这个例子:AppCrash.exe,当它崩溃的时候,它会在事件查看器中产生一个事件。在运行”命令窗口中输入“eventvwr”:
看看“常规”标签下的内容,有两点值得注意:
1、 错误应用程序名称:指明出错的程序,本例是AppCrash.exe。2、 错误模块名:指出此程序中的出错的模块。本例值AppCrash.exe。很明显可以看出是AppCrash.exe除了问题。假如错误模块是“AppCrashLib.dll”,那么我们将要调试此模块。
另一个重要的点是异常代码解释了到底这个错误是什么意思。在当前情况下,异常代码为0 xc0000005这意味着访问违例,这意味着应用程序试图访问无效的内存位置。所有异常代码的列表,请参阅下面的链接:
http://technet.microsoft.com/en-in/sysinternals/dd996900.aspx
第二步:获取崩溃转储(dump)文件
崩溃转储文件基本上包含了异常终止程序的当前工作状态。崩溃转储文件也能提供给我们当前内存的完整状态(例如RAM),可以用来分析出错的问题。最简单的获取转储文件是使用”procdump“(下载链接: Procdump)。”procdump“需要在程序崩溃之前进行设置: procdump -ma -x c:\dumps "E:\Study\Windows Internals\Training\Sample Code\AppCrash\x64\Release\AppCrash.exe".。这只是使用procdump的基本方法,还有更多的功能选项。目前的选项可以启动进程、获取程序崩溃时整个内存的dump文件,并存在C:\dumps目录下。
第三步:分析dump文件
我们已经获取了dump文件,现在来对它进行分析。分许dump最好的方法是使用“Windbg”工具。分析之前,我们需要拿到崩溃的exe对应的文件:pdb。pdb文件是程序的数据库,包含了需要调试程序的所有信息。唯一的需求就是pdb文件需要和exe文件一起编译的时候所生成,否则程序的数据符号不匹配,也就不能分析此dump文件了。
接下来我们启动Windbg,配置pdb文件(ctrl+s):
在Windbg窗口中,File->Open 打开dump文件:
加载dump文件之后便会出现以下结果:
在下面的命令中输入:!analyze -v:
现在我们集中注意力在各种输出信息来找出问题。我们看看STACK_TEXT这一段,它说应用程序崩溃,错误在AppCrash!main+0x13这里,但是并没有给出引起崩溃的源代码的位置。
再往下看,会发现有这么一段:这就给出了源码崩溃的具体位置。FAULTING_SOURCE_CODE: 5: { 6: int *p = NULL; 7: printf("This is start!\n"); 8: *p = 42; > 9: printf("This is end"); 10: return 0; 11: }
在上面的分析中,崩溃其实发生在第8行,但windbg指向第9行。这是编译过程启动优化的结果。
第四步:修复问题、发布
找出了问题所在,自然就知道如何对它修复了。
五、编译器优化
由以上知道由于编译器优化了代码,我们没能让windbg准确指出崩溃的地方。现在来看看编译器优化方面的知识。
我们可以设定编译器优化的等级。当选择"Full Optimization“优化选项时意味着产生的二进制文件(exe)的大小会减小,pdb文件的调试信息也会减少。反之,选择”Disable Optimization“时生成的程序会更大,pdb文件也会更大。Debug模式生成也是一样的情况。
总的来说,有四个选项进行配置。通常情况下,大多数项目的选项选择“Maximize Speed”,这是足够的对于调试崩溃的情况。在上面的例子中,如果我们关掉优化编译,我们会得到下面的结果:
在这里,我们看到了指向了正确的错误位置:*p=42.。出现这种情况的原因是有足够的调试信息来分析出错的根源。因此有一条规律就是:当我们发布应用程序时,我们应该保留一并生成的pdb文件,当出了问题的时候可以拿来进行分析。
六、pdb文件
对于任何非托管代码建,pdb文件与EXE文件一并生成。pdb文件包含了需要的调试信息。换句话来说,此文件也称为符号文件。符号文件包含了调试时候的不同符号。有局部变量、全局变量、函数名、源代码函数等等。每个信息都以符号而存在。有两种可用的符号:
私有符号:包括函数,局部变量,全局变量,用户定义的数据结构,源代码行号公有符号:函数,全局变量。
与私有符号相比,公有符号包含的信息比较少。公有符号只包含能够在不同文件中共享的信息。因此调用局部变量将不能成为公有符号的一部分。甚至在公共符号将大部分的功能有装饰的名称。
在私有符号中的调试会给出行号,而公有符号中则不会。大多数的公司做维护两个符号服务器:私有符号内部使用,公有符号对外发布。