CLR小组中存在着大量的回归测试,这些回归测试会定期执行来发现CLR中的Bug,Developer在Checkin之前,也需要执行这些测试的一部分(大概是10小时左右,如果全部跑的话估计要好几天)。这些测试对于保证CLR的质量是至关重要的。有时候,这些测试会偶尔失败,比如跑100次失败大概一到两次,有些极端的例子甚至是10000次才失败一次。像这种问题通常是很难调试的。在前面调试Bug的神兵利器:通过WinDbg条件断点收集Log这篇文章中,我讲到了如何通过条件断点收集各种信息来判断Bug究竟出在哪里。但是,这个方法还是不太管用,因为它不能够反复执行某个程序。下面我要讲一种技巧可以用来调试类似这样的问题,这种技巧主要适用于下面几种情况:
- 在程序出错的时候,某些信息、状态已经丢失,无法通过当前出错时候的状态推断出之前的状态。说的稍微具体一点就是,比如某个变量变成了NULL导致Access Violation,但是很难直接推断出为什么这个变量变成了NULL
- 程序运行时间较长,很难直接单步调试
- 程序较难修改加入打印代码(比如加入新代码并编译非常花时间,或者该程序没有源代码
- 该程序运行次数较多的时候才能发现问题,也就是说问题不是每次都出现
#2和#4决定了一步步调试基本上是不可能的。#1和#3则意味着我们必须得使用条件断点来收集信息来判断代码的错误,因为直接调试出错的位置是不可行的。下面了我来讲一下如何用CDB(其实就是WinDbg的无UI版本,WinDbg=CDB+UI)来做到:
- 反复执行程序
- 当程序出错的时候自动暂停
- 通过条件断点收集信息,只保留出错时候的那一次Log
我们先假设我们需要调试的程序叫做Hello.exe,每次出问题的现象是,调用某个函数Hello!Func()的时候,其参数arg为NULL。Arg这个变量是由某个全局变量g_arg传入而来。我们可以通过硬件的数据断点来查看每次将g_arg赋值为NULL的情况(当然了,赋值为NULL并不代表是错误,只有传入Hello!Func的时候为NULL才是错误)。程序一般要跑10000次才可能发现问题。使用下面的命令行可以做到反复收集Func1(Func2、Func3因为类似,这里就不列出了)执行时候的g_arg的值并放入Log文件中,并且如果发现调用Hello!Func的时候arg参数为NULL,则停止程序:
for /L %i in (1, 1, 10000) DO CDB.exe -c "bu Hello!Func /".echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g } /"; ba w4 Hello!g_arg /“.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }/”; g" -G -logo debug.log Hello.exe
我们来简单分析一下:
- 一开头的For语句用于执行CDB命令10000次,也就是调试Hello.exe一万次
- -c命令指定让CDB在程序开始的时候执行下面的命令
- bu Hello!Func “.echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g }意思是每次Hello!Func被执行的时候,打印Inside Hello!Func,之后打印所有局部变量和参数(包括arg),如果发现arg!=NULL,则继续。注意上面命令中的/”是转义符,代表真正的引号,避免冲突。
- ba w4 Hello!g_arg “.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }”意思是每次如果g_arg被修改成NULL,打印出Callstack
- g命令表示让程序开始执行
- -G:表示让CDB忽略程序结束的时候的Breakpoint,避免CDB在运行结束的时候停下,保证CDB可以持续执行不需要人工干预
- -logo debug.log:表示让CDB把每次输出的结果放入Debug.log中,并且每次都新建立文件,也就是说,会把上一次覆盖。这正好是我们需要的,因为我们设置了一旦程序错误则停止,那么这一次的Debug.log才是需要保留的
除了用-c指定初始的命令之外,也可以使用-cf来指定一个文件包含任意条CDB命令,如果CDB命令较多,可以采用这种方法。
本文说道的方法是比较有效的,我自己曾经使用过这种方法解决过不少比较棘手的问题。如果碰到了此种需要运行10000次才能重现问题的Bug,不妨试一下本文的方法。
P.S. 看到大家的评论,觉得有些东西需要稍微澄清一下:本文只是提供一种解决问题的思路,主要是在问题较为复杂,并且程序规模较大(>1百万),已有的Log信息并不够用的情况下对于C++程序(没错,CLR是使用C++编写的)采用。对于测试用例而言,输入是固定的,但是输出偶尔错误(举个简单例子,输入是1+1,但是输出99.99%是2,但是0.01%是3),像那种输入1+1输出总是3的情况不在本文讨论范围之内,毕竟单步就可以解决问题了。换句话说,本文的方法显然只是在某些特定情况下是比较合理的方法,并不代表应该应用在所有情况下。如果本文的方法能够能给大家提供一种解决问题的思路或者一些启示,那么本文的目的也就达到了。