怎样分析Windows dump

发布者:freakish
发布于:2017-05-18 22:33

dump文件从哪里来?

  • 项目工程
    • 一般的项目都会有类似xxxProtect 这样的工程,专门负责项目crash文件的搜集,上传操作。其主要原理就是利用Windows API MiniDumpWriteDump 来生成dump文件, 触发dump抓取时机就是程序发生异常的时候, 当然也可以在任意一个线程中的任意时刻使用这个API进行抓取
  • 专业抓取工具
    • Windows自带的任务管理就可以抓取,打开资源管理器,在任意一个进程上右键 - 创建转储文件即可
    • 其他类似专业工具

dump文件可以解决那些问题?

  • 程序Crash
    • 当程序需运行发生异常的时候,如果我们的搜集机制正确的完成dump文件搜集并上传到服务器,那么我们就可以从服务器获取到这个dump文件, 这个dump文件记录了程序发生异常的时候程序运行内存现场。因此通过分析这个运行上下文的快照,我们可以尝试查找问题发生的原因。
    • 有的时候,你会发现,用户反馈说程序莫名的就退出了,完全没有任何征兆,也没有弹出你们的dump文件搜集程序,这个时候只有一种原因,那就是程序发生了你们目前的搜集机制无法捕获的异常,这个时候应该怎么办呢?通常大家可以选择两个方案来尝试缩小问题排查范围:
      1. 通过客户端日志的最后一条消息输出来确定范围
      2. 如果用户肯配合,则可以通过给用户的机器上传Windbg这个轻量级的调试器,让用户在调试器下面运行,这样当用户发生异常的时候,就会被调试器捕获到,可以通过Windbg的dump生成命令生成dump文件来进一步进行问题排查, 参考Windbg dump命令:.dump /ma C:\dumps\myapp.dmp,其中/ma选项说明要生成内容详细的问题现场,但是生成的文件可能比较大。如果选择 /m选项,可能生成文件比较小,但是提供的信息就相对较少了,大家可以自己权衡。
  • 程序运行异常(如:界面卡死)
    • 在项目上线之后,可能我们都会遇到这样一种场景,就是程序没有发生崩溃,但是UI界面却没有响应了,这个时候你急切需要的就是要知道到底UI线程在干什么,为什么不响应我的操作了。你需要的第一手资料就是当前运行进程的内存dump文件,同样你需要先采用前面介绍的方法来进行内存抓取(资源管理器或者专业工具)
    • 有了内存执行快照dump文件,就可以开始根据经验来判断你首先要分析的是哪些对象了。就说界面卡死这种问题, 我们就是要看到底UI在这个时候在干什么这么忙,导致连我们的键盘鼠标这样的高优先级的操作都不响应了,比如我们会看当前UI线程的堆栈,看看最后是在做繁忙的任务,还是干脆发生了死锁。

分析dump之前,你需要哪些准备?

  • 工具
    • 自古是工欲善其事必先利其器, 所以要想从dump文件中获得重要问题信息,没有好的工具显然不行,我推荐 Windbg 和 VisualStudioxxx
    • Windbg
      • 这个工具其实不用多说了,微软亲儿子,Windows环境下的各种问题分析利器,dump分析其实只是他的一小部分功能,更多强大的功能大家可以自行百度,我们单说他的dump分析功能:
        1. 支持Windows系统中系统模块符号自动匹配下载(尤其问题发生在系统模块的时候很有帮助)
        2. 轻量级,整个工具包很小,方便传输携带,无论本地使用还是上传到用户环境似乎都是首选
        3. 支持pdb符号时间戳模糊匹配,这个很重要,尤其是无法找到当时打包的时候生成的pdb
        4. 配套的分析命令和插件,对分析问题很有帮助
    • Visual Studio
      • 宇宙第一IDE不是浪得虚名,提供了强大的代码编辑环境之外,同样提供了强大的调试支持,这里dump分析也属于她调试功能的一部分
  • 符号文件
    • 这个文件太重要了,对于分析问题能提供极大的帮助,那么问题来了:
      • pdb文件从哪里来? 在项目编译打包的时候,编译器会根据你的工程配置,在编译输出目录生成 .pdb文件,理论上每个独立的模块(.dll .exe)都会有一个对应名称的pdb文件,比如你的工程主程序是main.exe, 那么你应该会有main.pdb这么个文件,如图1:
      • pdb文件作用体现在哪里? 我觉得还是直接上图最能说明问题,如图2、3的对比起来就很清楚了(有符号能提供更多的崩溃现场堆栈调用信息):

常见dump场景分析实例

  • 实例场景一:程序崩溃分析
    • 这个例子我们选用Windbg来分析, 这里我假设已经获得了某个崩溃现场的文件,名称为A.dmp, 按如下步骤操作:
      1. 打开Windbg工具,选择菜单 File ->Open Crash Dump, 选择你的dump文件并打开,正常打开完毕,应该是这样的界面:
      2. 设置pdb符号, 选择菜单 File -> Symbol File Path, 在弹出的设置框中,填入你本地保存的pdb的路径,为了Windbg能够自动加载系统模块的符号,建议在你的pdb的路径之后添加分号,然后加入微软的系统模块符号路径,参考设置结果是这样的: d:\my_project\bin\release\pdb;SRV*c:\mysymbol* http://msdl.microsoft.com/download/symbols,前半段是你的工程的pdb目录,后半段是微软模块符号路径
      3. 如果分析的机器上有这个崩溃程序版本对应的源代码,那就更好了,你可以继续设置一下源码路径,设置源码路径有什么好处呢?你会很开心的发现,当你通过命令让Windbg分析crash现场的时候,Windbg能够自动化的给你定位到发生问题的那行源代码上, 但是请注意一个问题:如果你的源代码并不是当初编译这个程序的那份源码,Windgbg给你的结果就会迷惑你。举例来说,如果你有一个源码文件,有10行代码,这个源码在2017-06-01已经编译过程序A,并且已经发布上线了,然后你在2017-06-02这一天在这个源码的第2行前面增加了几行代码, 我们假定程序A在第5行发生了Crash, Windbg的自动分析就会给你自动定位到第5行, 但是其实这个时候因为你修改了源码,那么这个时候的第5行已经不是当初的第5行了,Windbg就会迷惑你了, 这同时也显示出了代码版本管理的重要性了。
    • 经过前面的几部操作,我们已经做好了最基本的准备,下面可以尝试一下Windbg的自动化分析了,为了保障符号的正常加载,我们首选让Windbg进入符号模糊匹配的模式,意思就是告诉Windbg我设置的这个pdb的路径是对的,如果pdb的时间戳对不上,你也给我强行加载吧,然后开始进入经典的三步(注意:如果左下角显示 BUSY, 就要等它一会儿,执行完这一步再进入下一步的命令):
      1. 进入符号模糊匹配模式:!sym noisy on,
      2. 加载pdb:.reload /i
      3. 自动化分析:!analyze -v
    • 如果你不清楚在哪里敲入命令,看我的图4(命令输入框位置),图5(你应该得到的结果):
    • 结果有了, 这个结果怎么看呢? 上面一幅图说过了,很简单,箭头的地方就是输入命令的地方,不说了。看下面的结果分析部分, 图中标出了 第一处 第二处 第三处三个地方:
      1. 第一处, 代表当前崩溃的最后一层函数调用是什么地方
      2. 第二处代表,根据当前设置的符号和源码路径,是崩溃在这一行了
      3. 第三处这里仔细看有一个 > 符号, 就是一个指示箭头,标识当前崩溃的行是在哪一行
    • 工具的帮助有了,下面就是开始依赖个人编程经验来分析了,首先看这一句 m_testPtr->SetEnable(false); 这样一句话的发生了崩溃,只有2中可能, 第一种:m_testPtr = NULL(或野指针); 第二种:m_testPtr所处的环境是一个异常环境,根据这里的实际情况就是, ClientLoginDialog对象的this指针是空(或野指针), 这个时候经过初步的判断,我们就可以选择不同的方法来排查了:
      1. 第一种就是直接看代码,查找m_testPtr是否可能为NULL, 或者 ClientLoginDialog对象的this是否可能为空或者是野指针
      2. 用Windbg继续排查, 这个时候继续输入一波命令:
        • 切换到异常线程:.ecxr
        • 打印异常堆栈:kvn
        • 这个时候,你应该进入这样的节奏了,看图:
        • 还是进入到了刚才的问题现场,这个时候我们输入 r 命令,接着输入u命令(目的是为了多打印一些崩溃点代码上下文),看崩溃现场详细信息:
        • 这个时候需要一点点汇编知识了,不然的话,你只有第一步中的那种方法来查代码了,我们直接看第二个箭头,
        • 00402fbc 8b01 mov eax,dword ptr [ecx] ds:002b:00000000=????????,观察第一个箭头 ECX = 0, 大家都知道 0地址内存是不能访问的,那到底是什么地方为0了呢, 接着看下面几句代码,从第二个红箭头的地方看到了Call, 这个是函数调用指令, 根据前面Windbg定位的 m_testPtr->SetEnable(false); 这句,我们知道肯定是在调用SetEnable这个函数, 而函数的地址来自 [eax+0ECh], 则只要EAX有效就行, 而EAX的值正是需要 崩溃的那一句 mov eax,dword ptr [ecx] ds:002b:00000000=????????来赋值, 所以这里我们可以知道, 这里的ECX 就是代码中的 m_testPtr 这个变量(这个因为所以的由来技术细节又来源于C++内存对象模型的知识,需要百度自行补充一下), 由此我们确定就是 m_testPtr 这个指针为空导致的问题,定位到这里,没有更多的路子走了,只能死看代码 + log来找问题了,Windbg也尽责完毕了。

光靠dump分析足够吗?

  • 从崩溃分析的部分可以知道, dump分析能告诉你是什么地方出了问题, 但是最后问题发生的具体逻辑原因是找不到的, 你需要继续看代码,然而经常看代码经常是很难找到问题, 所以你需要在开发的时候保持良好的log习惯,这个是关键时刻的救命草,好的log可以直接排查出来大多数问题,甚至都不用dump分析。
  • 所以,需要再强调一下的是,要有相对完善的log

声明:该文观点仅代表作者本人,转载请注明来自看雪