1、C/C+程序调试技术东方网力技术培训.内容提要l中断和异常中断和异常l调试断点调试断点l常见调试器功能常见调试器功能l源代码级别的主动调源代码级别的主动调试手段试手段lC+C+异常和异常和win32win32的的SEHSEHl常见程序问题调试常见程序问题调试l性能分析和优化性能分析和优化lC/C+C/C+语言的一些陷阱语言的一些陷阱l一些常见平台差异一些常见平台差异l实例分析实例分析l阅读程序的技巧阅读程序的技巧l几点建议几点建议.中断和异常中断和异常所谓中断是指所谓中断是指CPUCPU对系统发生的某个事件做出的一种反应,对系统发生的某个事件做出的一种反应,CPUCPU暂停正在执行的程序,暂停
2、正在执行的程序,保留现场后转入去执行相应的中断处理程序,执行完中断处理程序后再返回中断现场继保留现场后转入去执行相应的中断处理程序,执行完中断处理程序后再返回中断现场继续执行被打断的程序。续执行被打断的程序。中断可分为三类:中断可分为三类:1 1、第一类是由、第一类是由CPUCPU外部引起的,称作中断,如外部引起的,称作中断,如I/OI/O中断、时钟中断、控制台中断中断、时钟中断、控制台中断(重启动中断重启动中断,关机中断关机中断,),)等。等。2 2、第二类是、第二类是CPUCPU的内部事件,称作异常,如的内部事件,称作异常,如CPUCPU故障、程序故障故障、程序故障(非法操作码、非法操作码
3、、地址越界、浮点数溢出、除地址越界、浮点数溢出、除0 0错误等错误等)。3 3、第三类是由程序为了使用某些系统服务而主动引发,称作、第三类是由程序为了使用某些系统服务而主动引发,称作 陷入陷入(也叫软也叫软中断中断),如现在,如现在x86 CPU int 3x86 CPU int 3指令,指令,dosdos下著名的下著名的int 13int 13、int 21int 21等。程序调试断点等。程序调试断点就是通过就是通过int 3int 3指令实现的。指令实现的。4 4、x86 CPUx86 CPU的单步中断特性的单步中断特性(TRAP FLAG(TRAP FLAG被设置后,执行每条指令后都会发
4、生此被设置后,执行每条指令后都会发生此中断中断)。程序的指令级别的单步执行应该就是用单步中断实现的。程序的指令级别的单步执行应该就是用单步中断实现的。中断向量表中断向量表IDTIDT,即中断处理程序的入口地址表。,即中断处理程序的入口地址表。第三类软中断事件第三类软中断事件(异常异常)处理过程,以处理过程,以win32win32平台处理平台处理int 3int 3指令为例:指令为例:1 1、保留现场,进程、保留现场,进程/线程被挂起,进入操作系统的处理程序线程被挂起,进入操作系统的处理程序(执行系统执行系统int 3int 3的中断的中断处理程序,下面称为系统处理程序,下面称为系统)。2 2、
5、发生中断的进程如果处于被调试状态,则系统把、发生中断的进程如果处于被调试状态,则系统把int 3int 3事件通知给调试进程,尝事件通知给调试进程,尝试由调试进程处理试由调试进程处理int 3int 3事件。事件。3 3、尝试让进程自己处理、尝试让进程自己处理int 3int 3事件事件(参考参考C+C+的异常以及的异常以及WindowsWindowsS Structured tructured E Exception xception H Handlingandling知识知识)。4 4、如果、如果2 2、3 3情况都没处理情况都没处理int 3int 3事件,则系统弹出异常对话框,通知用户
6、进程发生事件,则系统弹出异常对话框,通知用户进程发生了异常了异常(此时用户可以使用调试器再来处理此时用户可以使用调试器再来处理int 3int 3事件事件 转入转入2)2)。第一类中断一般直接由系统处理,然后可能再分发给需要处理的用户进程。第一类中断一般直接由系统处理,然后可能再分发给需要处理的用户进程。第二类中断一般处理顺序为第二类中断一般处理顺序为 1-3-2-4.i 1-3-2-4.i.调试断点调试断点一般是通过调试断点一般是通过int 3int 3指令实现的。指令实现的。调试器设置断点原理调试器设置断点原理(以以VCVC调试器为例调试器为例):):调试器首先找到被调试进程需要设置断点的
7、指令地址调试器首先找到被调试进程需要设置断点的指令地址(调试版本根据调试版本根据源代码设置的断点也会被转化为实际的指令地址源代码设置的断点也会被转化为实际的指令地址),然后把该地址的,然后把该地址的1byte1byte数据记录到一张对应表里,接着把这数据记录到一张对应表里,接着把这1byte 1byte 改写为改写为0 xCC(0 xCC(即即int 3int 3指令码指令码)。这样当程序被调试运行的时候,在断点位置的指令其实就是这样当程序被调试运行的时候,在断点位置的指令其实就是int 3int 3指令,参指令,参照上一节的照上一节的int 3int 3中断事件处理过程,就可以明白调试器捕获
8、断点的工作机中断事件处理过程,就可以明白调试器捕获断点的工作机理。取消断点时则把对应表里记录的理。取消断点时则把对应表里记录的1byte1byte回写到被调试进程。回写到被调试进程。常用跟踪相关动作都是通过断点方式实现的常用跟踪相关动作都是通过断点方式实现的对于对于step tostep to、step over step over、step instep in、step outstep out等调试器都是通过在要运行的等调试器都是通过在要运行的下一个地址处先设置一个临时断点,然后调试运行程序来实现的。其它下一个地址处先设置一个临时断点,然后调试运行程序来实现的。其它断点实现类似。断点实现类似
9、。程序主动调试断点,程序主动调试断点,ASSERTASSERT宏,宏,ASSERT(false)ASSERT(false)即等效为一条即等效为一条int 3int 3指令。指令。理解和使用条件断点、单次断点、固定次数断点等。理解和使用条件断点、单次断点、固定次数断点等。怎么在动态库怎么在动态库(静态静态loadload和动态和动态load)load)里设置断点,里设置断点,VC VC的的Additional DLLsAdditional DLLs选项。选项。怎样在模板代码、内联函数、静态库代码里设置断点。怎样在模板代码、内联函数、静态库代码里设置断点。(调试器问题,怎调试器问题,怎样在不能设置
10、断点的代码位置设置断点样在不能设置断点的代码位置设置断点)。带调试信息模块和不带调试信息模块共存情况的调试方法,如带调试信息模块和不带调试信息模块共存情况的调试方法,如VBVB、浏览、浏览器、器、Media PlayerMedia Player使用我们需要调试的使用我们需要调试的.ocx.ocx、.dll.dll文件等。文件等。.常见调试器功能-具体参考具体参考VCVC,gdbgdb等调试器的用户手则等调试器的用户手则查看和修改变量,监视变量查看和修改内存,监视内存查看和修改寄存器,监视寄存器全局变量写监视Call Stack(调用堆栈)的查看更改指令指针寄存器EIP,实现调试时强行跳转(VC
11、 的 Set Next Statement命令同此)查看源码对应的汇编指令/机器码.源代码级别的主动调试手段编译时刻防御性编程编译时刻防御性编程-C+-C+契约契约(contractcontract)1 1、静态、静态assert(assert(编译时刻断言编译时刻断言)-STATIC_ASSERT()-STATIC_ASSERT(),must_have_base()must_have_base()2 2、一些有用的静态判断:、一些有用的静态判断:bool IS_INT_TYPE(T)bool IS_INT_TYPE(T)、IS_SIGNED_TYPE(T)IS_SIGNED_TYPE(T)、
12、GET_INT_MAX_VALUE(T)GET_INT_MAX_VALUE(T),见,见 npdebug.h“npdebug.h“调试时刻防御性编程调试时刻防御性编程宏宏ASSERT()ASSERT()、VERIFY()VERIFY()、TRACE()TRACE()等。等。MFCMFC的的AfxIsValidAddress()AfxIsValidAddress()、AfxIsValidString()AfxIsValidString()等。等。1 1、程序应该大量使用、程序应该大量使用ASSERT()ASSERT()宏,保证宏,保证ASSERT()ASSERT()覆盖覆盖没有正常处理的所有程序
13、逻辑分支。没有正常处理的所有程序逻辑分支。2 2、所有没有完成的函数和逻辑分支应该写上、所有没有完成的函数和逻辑分支应该写上ASSERT(false)ASSERT(false)以防止以后遗忘。以防止以后遗忘。运行运行/发布时刻防御性编程发布时刻防御性编程即程序的各种边界、容错、健壮性处理等。即程序的各种边界、容错、健壮性处理等。.C+异常和win32的SEH什么情况下建议使用异常什么情况下建议使用异常a a、当使用第三方提供的库,调用该库接口的、当使用第三方提供的库,调用该库接口的代码需要放在异常块里面代码需要放在异常块里面(对于第三方库内部对于第三方库内部有独立线程或独立进程时,目前我还没想
14、到好有独立线程或独立进程时,目前我还没想到好的办法增强程序健壮性的办法增强程序健壮性)。b b、构造函数可能失败的情况必须使用异常。、构造函数可能失败的情况必须使用异常。c c、在使用异常可以大大简化程序逻辑的地方、在使用异常可以大大简化程序逻辑的地方也可以使用异常。也可以使用异常。d d、内存分配可能失败的地方。、内存分配可能失败的地方。异常不可能全面代替错误处理。异常不可能全面代替错误处理。不可使用异常来做一般的逻辑控制。不可使用异常来做一般的逻辑控制。宏宏NP_BEGIN_CATCH_ALLNP_BEGIN_CATCH_ALL()()和和NP_END_CATCH_ALLNP_END_CA
15、TCH_ALL()().常见程序问题调试内存泄漏内存溢出/越界多线程死锁发布版本的调试分析只在发布版本才会出现的问题多平台调试.内存泄漏尽量减少对尽量减少对newnew和和deletedelete,mallocmalloc和和freefree的使用,尽的使用,尽量使用量使用C+C+的自动对象,如的自动对象,如std:string,std:vector,std:string,std:vector,class CAutoPtr,class CAutoObjclass CAutoPtr,class CAutoObj等。等。检查低级错误,通查程序里面的检查低级错误,通查程序里面的newnew、dele
16、tedelete、mallocmalloc、freefree等内存操作,等内存操作,deletedelete和和deletedelete是否混用是否混用如果对象有引用计数,查看计数是否有问题。可以如果对象有引用计数,查看计数是否有问题。可以使用调试器分析是谁在申请内存而没释放,使用调试器分析是谁在申请内存而没释放,VCVC里面里面可以直接在可以直接在C/C+C/C+运行库源代码里面设断点,其它平运行库源代码里面设断点,其它平台通过重载全局的台通过重载全局的newnew、deletedelete或者使用或者使用hookhook技术钩技术钩住住mallocmalloc、freefree后,再在重载
17、后,再在重载/钩子函数里设断点。钩子函数里设断点。用用VCVC自带的内存检测机制自带的内存检测机制(调试运行程序,正常退出调试运行程序,正常退出后检查内存信息后检查内存信息)。使用。使用Visual Leak DetectorVisual Leak Detector。其它方法,如打印、程序折半法等。其它方法,如打印、程序折半法等。.内存溢出/越界得到写越界/出错的内存地址(分为全局heap内存和函数局部stack内存),并监控该内存的内容,接着单步执行程序,找到引起该内存变化的语句,此语句就是导致内存越界的直接原因,然后再深入分析,找出真正bug。.多线程死锁理解程序发生死锁的机理。理解程序发
18、生死锁的机理。建议程序里面的线程同步对象全部使用建议程序里面的线程同步对象全部使用 npsync.h npsync.h、ILocker.hILocker.h、NPRWLock.hNPRWLock.h等代码库里面的函数,等代码库里面的函数,struct tagOSMutex:lockedThreadIDstruct tagOSMutex:lockedThreadID,即专为,即专为解决死锁而设计。解决死锁而设计。程序发生死锁后利用调试器的线程切换和堆栈查程序发生死锁后利用调试器的线程切换和堆栈查看能力配合看能力配合lockedThreadIDlockedThreadID信息,一般来说可以信息,一
19、般来说可以很快找到死锁原因。很快找到死锁原因。如果死锁实在不能避免,建议改造程序逻辑,使如果死锁实在不能避免,建议改造程序逻辑,使用用TryLockTryLock、SendTimeoutSendTimeout等方式。等方式。.发布版本的调试使用map文件。使用手工插入软断点 int 3,直接查看汇编指令。查看程序CPU、内存、各种句柄使用情况(windows的任务管理器,linux top命令)。原始方式:打印,printf()、OutputDebugString().分析只在发布版本才会出现的问题A首先需要理解发布版本和调试版本的不同。首先需要理解发布版本和调试版本的不同。发布版本没有任何调
20、试相关代码,检查是否有错发布版本没有任何调试相关代码,检查是否有错用用VERIFYVERIFY为为ASSERTASSERT的地方。的地方。发布版本一般的内部不会有初始化动作,而在调发布版本一般的内部不会有初始化动作,而在调试版本,编译器为了便于调试,一般会对内存做试版本,编译器为了便于调试,一般会对内存做初始化。初始化。如如VCVC在调试版本会用在调试版本会用0 xCC0 xCC初始化所有自动变初始化所有自动变量,用量,用0 xCD0 xCD填充填充newnew出来的内存,用出来的内存,用0 xDD0 xDD填充填充deletedelete的内存,用的内存,用0 xFD0 xFD填充受保护的内
21、存填充受保护的内存(动态分动态分配内存的前后地址配内存的前后地址),以上值都是比较大的奇数,以上值都是比较大的奇数,这样便于查错。这样便于查错。.分析只在发布版本才会出现的问题B发布版本会优化掉一些不必要的操作和变量。发布版本会优化掉一些不必要的操作和变量。如优化掉一些局部变量就会引发一些只会在发布版本发生如优化掉一些局部变量就会引发一些只会在发布版本发生的错误,如:【理解的错误,如:【理解x86x86体系的体系的CPUCPU的堆栈地址是递减的,的堆栈地址是递减的,著名的著名的c c语言语言bufferbuffer溢出攻击即是基于此理】溢出攻击即是基于此理】int a;char ch4;/*/
22、int a;char ch4;/*/变量变量a a没有使用或者只是在调试版本使用,当对没有使用或者只是在调试版本使用,当对chch发生向发生向后越界操作时后越界操作时(小于小于4bytes4bytes的越界的越界),在调试版本因为有变,在调试版本因为有变量量a a,不会产生错误,但发布版本,不会产生错误,但发布版本int aint a可能被优化掉,则可能被优化掉,则会引发堆栈错误。会引发堆栈错误。检查有使用检查有使用#ifdef _DEUBG#ifdef _DEUBG的地方是否会导致调试版本和的地方是否会导致调试版本和发布版本有逻辑差异。发布版本有逻辑差异。也有可能因为使用系统库的不同,如也有
23、可能因为使用系统库的不同,如MFCMFC库,引发一些差库,引发一些差异性错误。异性错误。.多平台调试若程序不是特别平台相关,应尽量让程序可以在多个平台下编译运行,比如在linux平台不易查的问题,可以到win32平台下来查。尽量使用标准C库、stl以及codelib里面的跨平台库。.性能分析和优化A利用利用x86x86的的RDTSCRDTSC指令进行精确的时间统计指令进行精确的时间统计(在多核在多核CPUCPU系统下使用该指令需要小心系统下使用该指令需要小心)。对程序进行时间复杂度统计。对程序进行时间复杂度统计。利用编译器提供的统计功能,如利用编译器提供的统计功能,如gccgcc的的GPROF
24、GPROF(参考(参考MakefileMakefile)。)。使用调试器配合软断点使用调试器配合软断点(int 3)(int 3)查看发布版本的汇编代码,查看发布版本的汇编代码,了解代码在发布版本里对应的实际执行指令。了解代码在发布版本里对应的实际执行指令。优化,找到关键问题所在,记得二八原则,即程序优化,找到关键问题所在,记得二八原则,即程序80%80%的的时间在执行时间在执行20%20%的代码。写一个模块的代码。写一个模块(函数、类函数、类)的时候,的时候,时刻想到是可读性重要,还是性能重要。时刻想到是可读性重要,还是性能重要。分清楚什么时候该用分清楚什么时候该用ASSERTASSERT,
25、什么时候该用错误处理逻,什么时候该用错误处理逻辑,常见的做法是在最底层函数使用辑,常见的做法是在最底层函数使用ASSERTASSERT声明所有的声明所有的非法情况,在上层函数使用错误逻辑处理,这样既保证了非法情况,在上层函数使用错误逻辑处理,这样既保证了正确性,也获得了发布版本的效率。例:在正确性,也获得了发布版本的效率。例:在TYPE*TYPE*CAutoObj:operator-()CAutoObj:operator-()对对象是否合法的对对象是否合法的ASSERTASSERT检测,检测,为了效率,此处显然不应该用错误处理为了效率,此处显然不应该用错误处理(非法时返回非法时返回NULL)N
26、ULL)。.性能分析和优化B没必要在某些问题上耗费我们的时间,现代编译器对于有些优化比你没必要在某些问题上耗费我们的时间,现代编译器对于有些优化比你做的更好,如:做的更好,如:a/2-a1a/2-a1,没必要把整数乘法变成位移,破坏了,没必要把整数乘法变成位移,破坏了程序可读性,这件事情编译器会帮你做。时刻谨记编译时刻常量程序可读性,这件事情编译器会帮你做。时刻谨记编译时刻常量(C+(C+摸板元编程摸板元编程)是不会耗费任何执行时刻开销的,比如定义是不会耗费任何执行时刻开销的,比如定义#define XXX#define XXX(12)(12)就比就比#define XXX 0 x8#defi
27、ne XXX 0 x8可读性好。可读性好。尽量减少大数据拷贝动作,在读磁盘和内存缓存之间做权衡。减少网尽量减少大数据拷贝动作,在读磁盘和内存缓存之间做权衡。减少网络访问次数和传输的数据量。络访问次数和传输的数据量。怎么节约内存和避免内存碎片【在服务器程序和内存受限系统中这是怎么节约内存和避免内存碎片【在服务器程序和内存受限系统中这是个重要问题】个重要问题】1 1、内存池、重载、内存池、重载newnew和和deletedelete,class SameSizeMemMgrclass SameSizeMemMgr。2 2、在堆栈够用的情况下尽量使用堆栈内存,即尽量使用局部对象、在堆栈够用的情况下尽
28、量使用堆栈内存,即尽量使用局部对象 这这也有利于编译器优化也有利于编译器优化。如当一个数值的长度是常数。如当一个数值的长度是常数 编译时刻确定的编译时刻确定的数数,则一般使用局部数组。推荐大家尽量使用,则一般使用局部数组。推荐大家尽量使用class CSmartBufclass CSmartBuf和和class CSmartArrayclass CSmartArray来定义局部数组对象。来定义局部数组对象。3 3、尽量减少、尽量减少newnew和和deletedelete的使用,建议使用自动对象包容摸板的使用,建议使用自动对象包容摸板class class CAutoObjCAutoObj。.
29、C/C+语言的一些陷阱A整数整数1 1、回环问题,怎样用、回环问题,怎样用ticktick统计时间长度;统计时间长度;2 2、扩展问题,如、扩展问题,如1616位扩展到位扩展到3232;3 3、位移问题,如、位移问题,如:int32 33:int32 33不是我们想象的不是我们想象的0 0,而是与,而是与int32 1int32 1等等价,因为价,因为c/c+c/c+编译器为了效率直接使用了硬件移位,而很多硬件指编译器为了效率直接使用了硬件移位,而很多硬件指令的位移就是这么做的。另:在令的位移就是这么做的。另:在3232位平台下位平台下VCVC和和gccgcc实现的实现的INT64 INT64
30、 对对于于 int64 65 int64 65结果为结果为0 0 而不是等价于而不是等价于 int64 1 int64 1。宏,别忘了在宏里面加括号,如:宏,别忘了在宏里面加括号,如:#define XXX(a)a10#define XXX(a)a10 则必须写为则必须写为#define XXX(a)(a)10)#define XXX(a)(a)10)。宏不是函数。宏不是类型定义。宏不是函数。宏不是类型定义。C+C+的自动类型转换,如果不想被隐含使用,就在构造函数前面加上的自动类型转换,如果不想被隐含使用,就在构造函数前面加上explicitexplicit,struct A A(int i)
31、;struct A A(int i,int b=0);struct A A(int i);struct A A(int i,int b=0);是需要加是需要加explicit explicit 的两种形式。小心使用的两种形式。小心使用 operator TYPE()operator TYPE()。对对VC6 forVC6 for语句语句bugbug的修正的修正#define for if(0)(void)0);else for#define for if(0)(void)0);else for 见见 msc_opt.h“msc_opt.h“。intel CPUintel CPU浮点数和浮点数和
32、mmxmmx问题,问题,emmsemms指令。指令。比比sizeof(ar)/sizeof(ar0)sizeof(ar)/sizeof(ar0)更好的数组长度计算子更好的数组长度计算子 ARRAY_LEN,ARRAY_LEN,见见 “nprbase.h”“nprbase.h”。.C/C+语言的一些陷阱B关于关于0 0的特殊用法,在的特殊用法,在C+C+里,里,0 0可以是可以是intint类型,类型,也可以是任何指针类型,如也可以是任何指针类型,如NULLNULL一般定义如下一般定义如下#define NULL 0#define NULL 0,就会导致如:,就会导致如:int index=in
33、t index=NULLNULL;这样就可能有潜在逻辑错误的代码被编译;这样就可能有潜在逻辑错误的代码被编译过过(正确应该是正确应该是int index=-1)int index=-1)。boolbool和和BOOLBOOL问题,问题,sizeof(bool)sizeof(bool)和和sizeof(BOOL)sizeof(BOOL)可能不等,可能不等,BOOLBOOL和和intint可能是同一个类型。可能是同一个类型。std:stringstd:string作为作为printf()printf()的可变参数的可变参数(C(C语言可变参函语言可变参函数语法陷阱数语法陷阱)会导致程序崩溃。会导致
34、程序崩溃。语言库提供的某些随机函数可能是非均匀分布的,语言库提供的某些随机函数可能是非均匀分布的,如:如:rand()%100rand()%100并不是在并不是在0,100)0,100)之间均匀分布,之间均匀分布,在需均匀分布的时候请使用在需均匀分布的时候请使用RandNum()RandNum()和和RSort_()“QSort.h“RSort_()“QSort.h“,这些函数保证了伪随机数的,这些函数保证了伪随机数的均匀分布性。均匀分布性。.一些常见平台差异Win32Win321 1、写、写socketsocket绑定事件问题。绑定事件问题。2 2、WaitForMultipleObject
35、s()WaitForMultipleObjects()事件数量限制。事件数量限制。3 3、stl bugstl bug,见,见“npr_std_mender.h”“npr_std_mender.h”。4 4、fopen(,“w”)fopen(,“w”)文本模式问题,请使用文本模式问题,请使用 “npfdknpfdk.h”h”。LinuxLinux1 1、线程没有优先级别,创建线程默认堆栈、线程没有优先级别,创建线程默认堆栈.实例分析.阅读程序的技巧从功能、界面、通信协议及命令码入手查找关键从功能、界面、通信协议及命令码入手查找关键代码代码(如对一个如对一个win32win32程序,可从菜单文字
36、找到资程序,可从菜单文字找到资源源IDID,再用资源,再用资源IDID找到其处理函数,找到其处理函数,)熟练使用代码管理工具:熟练使用代码管理工具:VC+VC+助手、助手、UltraEditUltraEdit、Araxis MergeAraxis Merge、Source InsideSource Inside、vi vi、findfind、grepgrep、SVN.SVN.使用调试跟踪手段来熟悉程序逻辑。使用调试跟踪手段来熟悉程序逻辑。建议熟练使用查找,包括文件建议熟练使用查找,包括文件 单单/多多、文件内容,、文件内容,使用正则表达式查找,参考使用正则表达式查找,参考VCVC和和UltraEditUltraEdit等等整理程序技巧,熟练使用编辑工具的替换功能。整理程序技巧,熟练使用编辑工具的替换功能。技巧:怎样使用技巧:怎样使用VCVC在多文件中进行正则表达式替在多文件中进行正则表达式替换。换。.几点建议复现错误复现错误描述错误描述错误始终假定错误是自己的问题始终假定错误是自己的问题进行有创见的思考进行有创见的思考分解并解决错误分解并解决错误使用调试辅助工具使用调试辅助工具开始调试工作开始调试工作确认错误已被更正确认错误已被更正经常学习和交流经常学习和交流.