调试的艺术——Debug技巧总结
(本文从写好的wiki里粘出来的,格式稍乱不影响阅读)
用Q+编号代表问题,A+编号代表答案。用这种方式组织。如无特别说明,这些技巧都是针对Visual Studio 2003的
汇编级的问题我作为一个逻辑程序只能说略知皮毛,内容仅为抛砖引玉,说法不严谨之处希望能毫不客气的指出,以便改正。但大部分信息都是有经验或参考资料确认的,有问题可以与我探讨。Q1:Release版本不能调试吗? A1: Release版本、Debug版本的区别,据我目前所知有3处:编译是否“编译器优化”过;是否有完整调试信息;_DEBUG宏和NDEBUG宏;先说这三个选项的位置: “编译器优化”,VS2003里,位于 工程属性(在Solution Explorer面板的某个工程上点右键)-> C/C++ -> Optimization -> 第一项Optimization。一般Debug版设置为Disabled,Release版设置成Full Optimization。 “是否有调试信息”,VS2003里,位于 工程属性 -> C/C++ -> General -> Debug Infomation Format。Release版默认是Disabled,Debug版用最完整的Program Database for Edit & Continue。 _DEBUG宏和NDEBUG宏,VS2003里,位于 工程属性 -> C/C++ -> Preprocesser -> Preprocesser Definitions。Release版一般设置为NDEBUG,Debug版设置为_DEBUG “编译器优化”,是指编译器在编译过程中,并不按照源码逐行翻译成汇编,而是为了效率对代码做出更改,比如:消除不必要的局部变量,去掉完全没有影响的代码。现代编译器已经发展的非常完善,甚至可以做到调整循环次序、把循环内的语句提到循环外、消除函数调用等等,可以在保证结果完全一致的情况下达到最高效。 值得注意的是: 1.越是高级的编译器,优化功能越强大,VS2010和VS2003就有不小的区别,可以写些简单的算法,然后看看汇编结果。2.甚至对于“高手”来说,编译器优化的代码往往比人工优化的代码要更好(因为写编译器的是优化砖家),所以写代码时,应该更关注算法方面、函数调用结构方面的问题,不必过于关注局部变量之类的细节,局部问题编译器会做的很好。 所以有些人用Release版调试的时候,发现执行顺序非常怪异,和源码对不上,其实就是编译器优化造成的。 “是否有调试信息”,决定了pdb文件是不是完全,pdb文件既要记录源代码的位置,以便跟踪,还得记录一些额外的地址信息,以便显示出局部变量的值。有时候调试Release版本,看不到变量的值,原因——要么是这个变量已经被优化没了,要么是调试信息不全造成的。 _DEBUG宏和NDEBUG宏,这个比较简单,DEBUG版本在编译时,会自动定义一个宏#define _DEBUG,所以有些代码希望只在debug版生效时,只需要 #ifdef _DEBUG //debug版才编译的代码 #else //release版需要的代码 #endif 这样写一下就可以达到目的了,最常见的是 assert 宏。assert在寻仙底层代码非常常见,assert函数只在debug版被执行,release版被忽略。所以是个开发时测试用的好东西。如果在release版也暂时需要用assert,就修改一下VC设置,把NDEBUG宏改为_DEBUG就行了,不过这样修改也可能会影响其他代码。(另外还有一些现象,我还没有搞清:为什么DEBUG版的局部变量会被初始化成0,而release版不初始化,是通过哪个选项控制的,求解答。)调试Release的意义在于:有些BUG只有在Release方式下能重现,所以搞清楚Release和Debug的区别,适当修改设置,关键时刻显得非常重要。
Q2:想输出调试信息,不知道应该输出到哪里
A2:控制台程序很容易通过printf或者std::cout输出信息,但是windows窗口程序比较麻烦。写windows小工具时,可以用MessageBox输出信息,不过对于寻仙这种大型游戏恐怕太不合适了。 能输出LOG,就用LOG输出 逻辑层的模块,大部分可以使用LOG模块,建议多输出LOG进行调试。查看log文件务必采用UltraEdit、Notepad++、Vim之类能够自动检测到文件变化并重新打开log文件的工具,以便随时查看输出,用记事本可能会给自己带来麻烦。不能输出LOG,用OutputDebugString 底层模块不能输出LOG,想输出LOG的话源码修改太多过于繁琐。这时候也有绝招:windows.h 头文件,提供了一个输出信息的函数:OutputDebugString(包括两个,OutputDebugStringA和OutputDebugStringW,分别支持MultiByte和WideChar)。需要使用时,只要在你需要调试的cpp文件里包含windows.h,然后自己拼装字符串,输出即可。 查看OutputDebugString的输出结果。OutputDebugString的结果,输出到哪里了呢?答案是“系统调试信息”,这玩意如果用VC调试程序,可以在Output窗口里看到。如果不调试,如何看到呢?答案是用“DebugView”小工具。上网搜一下应该能找到,也可以问别人要。 OutputDebugString也失效。由于Windows.h文件过于庞大,有可能造成和源文件冲突编译不过,这种情况。。。只能具体问题,具体解决了。问题总比方法多(龚大侠语录)。不使用调试器的原因是:调试器很可能会在关键时刻不给力,特别是内存已经写乱的情况。过于依赖调试器不是什么好事。Q3:我在调试core dump文件,程序是release版的,代码被优化过,变量的值全都看不见,执行顺序也不清楚,只能看到汇编代码,怎么办?
A3:这种情况,如果不想(不能)放弃的话,就埋头看汇编代码吧! 如果运气不是太差,源码能和core dump文件对应上,VS2003就可以打开 汇编、C++ 的混合视图,可以对照着看。 1.崩溃基本上肯定是访问了不可访问的地址。所以……专心看访问哪个地址时出错了,这个地址的值是怎么一路算过来的。 2.分析每一句C++代码和哪些汇编代码对应。特别的,if、for、while在汇编里最终肯定表现为跳转(jmp、jne之类),所以看懂了跳转,就明白了执行流程。还有函数调用(call)也可以帮你定位。 3.如果发现C++和汇编不容易对应上,也不要惊慌,很可能是优化导致一些函数被展开了,考虑到优化以后再分析分析即可。非常有效的方法:找个和你水平差不多或者比你更懂汇编的人,一起研究。可以拓宽思路,也可以互相鼓励。Q4:来不及下断点怎么办?下断点断不下来怎么办?
A4:我以前一直认为,如果自己有代码,想用断点就总是可以断下来的,实在不行启动程序以后用attach也总是可以调试的。但是事实证明,如果程序员有N种调试程序的方法,那么大自然就有N2种调戏程序员的方法。例如: 1.程序逻辑和系统时间密切相关。比如调试冷却时间CoolDown这种的,如果CoolDown时间只有1秒,基本你断下来断点就2秒了,你可能总是无法进入你想进入的那条逻辑分支。还有比如网络层,调试时间非常有限,5秒就超时,非常尴尬。 2.操作系统抽风了,怎么都断不下来。 3.错误是在程序刚启动时出现的,如果从VS2003里启动,就不会出错;如果不从VS2003启动,就会出错。但是你启动程序再Attach也来不及,因为从启动到出错只有半秒时间。 4.错误重现概率极低,重复十次出现一次,而且直接从VC2003启动程序就更难重现了。一般来说,前两种现象还是可以解决的,第一种情况可以改改逻辑,给自己多一些时间。第二种情况查一下系统服务,甚至重装一下VS,还是有办法的。第三种情况我在这个月刚刚遇到,发生在关键的变量是个随机值的情形。(而且这个现象说明从VS启动程序、直接启动程序、和attach之后,三种情形程序内存布局有着微妙的区别。)第四种情况曾发生在内存访问越界的情形。是因为访问非法内存时,操作系统有时候可以检测到异常,有时候不会。有几种方法,列举出来,但具体怎么用还得发挥聪明才智 :) 1.强制断点 INT 3。 具体做法:在程序里加一句__asm int 3 ( 如果你在用 gcc 可以加 asm ("int $3"); ),程序执行到这里会崩溃,然后windows会问你是否需要调试,然后你就可以从容的用VS2003进去调试了。很好的办法,自己写工具时不妨一试。可惜的是这种做法在咱们的项目里无效,因为这个异常会被底层catch到(JException::init()),然后打印一堆信息以后退出……没有调试的机会。 2.assert(表达式)、OutputDebugString等。如果你对问题已经有了大概想法,比如“这个地方如果index是3,那么就会越界错误了”,那么赶紧加一条assert(index==3),在DEBUG模式下试试看你猜想的对不对。而且assert不要删除,因为它在release模式下不会被运行。写一些有意义的assert对代码健壮性很有帮助。 3.加一句if。加一句if,并在if的{}里加一些没意义的代码,比如int i=0; ++i;然后把断点下在++i上。很常见的做法,但是如果你遇到非常诡异的情况,debug不容易重现,关闭release的优化选项也不容易重现,你就只能开着优化,但是优化会把你那句无用的代码整个删掉。这时候……不妨用下面while(true);的方法。 4.while(true);,用assert的不方便之处在于,一旦代码进入assert里面,就不好回到之前的地方了;用上一条办法在开着优化的情况下也不好办。而往往捕捉到错误之后你希望能继续单步调试。所以,不妨在捕捉到错误之后,让程序进入死循环,然后程序会卡住等你进去慢慢调试了。开始调试的时候,VS2003可以拖曳那个箭头改变当前执行的代码,从死循环里跳出来。或者你用一个很多次数的空循环,比如100000000次,或者有些程序可以用sleep(10000),思路都是一样的。Q5:调试的时候,在watch里查看对象的成员变量的值,但是感觉watch的语法与C++不同,有什么技巧吗? A5:watch我也用的不是很好。用watch需要解决几个问题: 调试STL容器,我只会调试vector,调试vector的方法类似于调试C++数组,比如有个数组a[10],监视里面的10个值: a[0],10 像这样写就行,vector也差不多。VS2003调试map、list时不是很给力。求解。调试对象的成员变量里面的成员变量里面的成员变量……;由于watch里面对"->"的支持不是很好,写一大串语句watch识别不了。超哥传授了一个绝招:使用quick watch,在quick watch里选择成员变量时,quick watch输入框里的文字也会变……原来输入框里的东西就是你想要的变量的表示方法。把它粘到watch窗口里试试吧! 在watch里面调用函数,可能会造成诡异的崩溃。因为watch里调用函数也可能会遭遇异常。调试时遇到奇怪的崩溃可以看看是不是自己在watch里写了不该写的东西。Q6:遇到非常费解的客户端或服务器很卡的现象,不知从何入手 A5:如果程序速度突然变得非常明显的慢,而且可以重现,那就可以运行以后用VS attach到进程里,然后Break All,多试几次(碰概率)。看看是否多次卡在同一个地方。如果多次卡在同一个地方,那么就可以大胆的猜测和试验了。虽然这种方法很傻,但是至今用这种方法调试出来的问题有:1、技能系统错误填写,导致循环10000000……帧;2、客户端在外服运行遭遇大量LOG导致卡死。据我所知已经有这两个成功的经验,实乃神技。