前言
我们使用调试工具的时候,一般是要处理以下问题:
- crash
- anr application no response
- memory leak
- dead lock
- high cpu usage
- …
Windows平台上有很多对应的处理方案,不过现有的方案一般都有一个通病:一旦打开某些调试功能或者系统设置,调试目标的性能就会成倍下降。这一点有时候会对调试来带很大的干扰,比如出现目标程序UI线程卡顿,导致无法正常操作,或者没发现要调查的问题,反而是发现了一堆性能方面的问题。
crash
崩溃的情况可以从几个正交的维度来划分:是否必现,开发的机器上是否可以复现,是否抓到crash dump。
一旦接手一个crash问题排查,可以按照下面的步骤来排查:
- 发现问题的机器上是否可以复现。可以复现的情况又分为:
- 必现 : 这个情况是最简单了。
- 高概率复现 :这个也比较简单。
- 低概率复现,甚至只出现一次 :这个最棘手,作为开发最怕遇到这类问题。但是理论上讲,只要能发现一次就一定有一个必现的路径,只不过实践中碰到的概率低而已。套用一句话来描述就是:crash只有0次和无数次。
- 能复现的话,是否有复现路径。
- 有的话,开发的机器上是否可以复现:那么直接用调试器来看就好了,这个最简单了,调试器就可以直接抓到crash点。
- 开发的机器上不能复现的话,qa的机器上是否可以复现:可以通过远程调试的方法来处理。
- 如果开发和测试的机器都不能复现的话,可能就需要联系客户了。这个要做好准备,这时候可以使用的方法就是:日志+dump。
- 使用dump还要看是否能(尽量)跑debug版本,因为有时候release版本缺少一些符号信息,可能会出现即使复现了,但由于缺少符号,缺少active信息而导致无法进一步查问题。
- 提一下,日志对于crash问题来说往往帮助不大,不过聊胜于无。
- 走到这一步,那就是说这个问题很可能不好复现,而且拿不到dump信息,那么这时候问题的解决就很可能没希望了。留下最后一个办法就是:耐心。排查代码,不论使用什么方法,在怀疑的点上加日志,做好准备抓dump,部署到目标环境,然后静候crash发生。
对于上面的场景,其实概括起来处理方法就三种:
- 本地调试:一般是使用MSVC
- 远程调试:MSVC或者WinDbg都可以
- 抓dump分析:MSVC或者WinDbg都可以,生成dump的方式,可以在资源管理器里面找到目标进程,右键执行创建转储文件;或者使用MSVC/WinDbg的创建dump功能。
anr
首先,对于anr的问题要注意的一个问题是,一定要确定anr确实发生了,因为有时候可能是程序运行环境配置较差导致的,静置一会儿看看是否就没问题了。当然如果干脆就不允许出现anr,那么应该有两方面的考虑:给出最低环境配置和找出造成anr的问题代码(比较有追求的做法)。
确定是anr问题了,那么接下来就要采取一些措施,比如动态抓dump看UI线程卡在哪里了。或者直接制造一个crash,也是得到dump,调查一下UI线程卡在哪里了。当然如果能上(本地或者远程)调试器的话是最好的了,anr的时候直接让程序停下来就可以在call stack中看到UI线程在干嘛。
另外提一下,mac上有一个不错的工具,可以在活动监视器里面采样,从而直观地看到哪行代码有问题。Windows系统上就只能用msvc工具来查了,但毕竟并不是所有(测试/客户的)环境都有调试工具,不是很好查;同时这个问题还可能与high cpu usage或者dead lock有点关联。
memory leak
内存泄露问题,可以:
- 使用msvc的Heap Profiling工具,通过take snapshot不同时间点的内存快照,进行对比得到内存泄露的点。
- Windows 10上可以使用UMDH工具来做类似的动作。
- Dr. Memory,下载地址
- Top 20+ Memory Leak Detection Tools For Java, C++ On Linux And Windows
- 通过Application Verifier工具中的内存相关功能查看。通过WinDbg配合Application Verifier来调试是一个不错的组合,WinDBg可以及时地查看出问题时对应句柄等资源的信息。
- 使用 Debug Diagnostic Tool,根据指定的内存阈值抓取dump,这个工具的另一个名字叫 “DebugDiag”。参考 Memory dumps to study a high-CPU performance issue,使用这个工具需要对系统的各种计数器的含义比较了解。调试诊断工具 (DebugDiag) 旨在帮助解决诸如挂起、性能下降、内存泄漏或内存碎片以及任何用户模式进程中的崩溃等问题。
- Physical and Virtual Memory in Windows 10 介绍Windows 10上内存知识很全面的一篇文档,里面有很多精美的插图。
- Deleaker Blog Visual Studio 20xx的一个付费插件。
- 微软官方工具介绍的一个视频集合:Defrag Tools
- 原本在channel9上的两个内存管理的视频:Mysteries of Memory Management Revealed Part1,Part2
- A story of fixing a memory leak in MiniDumper 使用VMMap + WPT分析内存泄露的一个不错的例子。
- milostosic/MTuner is a C/C++ memory profiler and memory leak finder
dead lock
发现死锁本身就是一个问题,比如非UI线程死锁,可能就没有UI线程卡死那么直观。可能是某些对该线程的同步调用卡住,或者异步调用没有结果返回才能发现。
发现之后可以抓dump,或者上调试器。
high cpu usage
Windows上由于缺少一个对进程的各个线程采样统计的工具(我没发现),所以这个问题查起来没有那么直接。WinDbg上可以用!runaway来查CPU的使用时间,但是这个使用时间,我们无法指定开始统计和结束统计的时间点,也就是说真正耗时的操作可能会被很早运行的线程运行时长超过,导致我们定位不出来。
Profile
手工复原堆栈
堆栈失效,可能是由于对无效地址的调用导致调试器丢失了返回地址的位置;或者您可能遇到了无法直接获取堆栈跟踪的堆栈指针;或者可能存在其他一些调试器问题。
思路是:
- 先从检查符号开始 x *! 命令
- dd esp/rsp 查看栈指针附近的信息,找出可能是函数指针的值,这里要注意排除掉下面的值:
- 一般整数比较小,dd的时候大部分位是0,这类值不太可能是函数地址
- 英文字符的范围一般在 [20,7f] 区间
- 指向栈上的指针变量大小一般与esp/rsp接近
- 函数指针一般不太可能重复
- 状态码通常以 ac (c00000d6) 开头
- 最后,栈的地址应该位于模块的区间内,也就是在第一步x命令找到的模块的地址区间内
- 这样得到一组函数指针备选集,然后逐个使用ln address查看对应符号,得到一组可能的函数调用符号
- 然后通过反汇编指令u 函数指针备选集合中的每一个元素的前一个地址,看看得到的结果是否匹配上面那组符号,从而构建出调用堆栈来。
GFlags
Windows上好多调试功能都需要 gflags 这个工具。
GFlags(全局标志编辑器)gflags.exe 启用和禁用高级调试、诊断和故障排除功能
gflags官方有一个实例说明:GFlags Examples