资源预览内容
第1页 / 共23页
第2页 / 共23页
第3页 / 共23页
第4页 / 共23页
第5页 / 共23页
第6页 / 共23页
第7页 / 共23页
第8页 / 共23页
第9页 / 共23页
第10页 / 共23页
亲,该文档总共23页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述
1,C/C+程序调试技术,东方网力技术培训,2,内容提要,中断和异常 调试断点 常见调试器功能 源代码级别的主动调试手段 C+异常和win32的SEH 常见程序问题调试,性能分析和优化 C/C+语言的一些陷阱 一些常见平台差异 实例分析 阅读程序的技巧 几点建议,3,中断和异常,所谓中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保留现场后转入去执行相应的中断处理程序,执行完中断处理程序后再返回中断现场继续执行被打断的程序。 中断可分为三类: 1、第一类是由CPU外部引起的,称作中断,如I/O中断、时钟中断、控制台中断(重启动中断, 关机中断, )等。 2、第二类是CPU的内部事件,称作异常,如CPU故障、程序故障(非法操作码、地址越界、浮点数溢出、除0错误等)。 3、第三类是由程序为了使用某些系统服务而主动引发,称作 陷入(也叫软中断),如现在x86 CPU int 3指令,dos下著名的int 13、int 21等。程序调试断点就是通过int 3指令实现的。 4、x86 CPU的单步中断特性(TRAP FLAG被设置后,执行每条指令后都会发生此中断)。程序的指令级别的单步执行应该就是用单步中断实现的。 中断向量表IDT,即中断处理程序的入口地址表。 第三类软中断事件(异常)处理过程,以win32平台处理int 3指令为例: 1、保留现场,进程/线程被挂起,进入操作系统的处理程序(执行系统int 3的中断处理程序,下面称为系统)。 2、发生中断的进程如果处于被调试状态,则系统把int 3事件通知给调试进程,尝试由调试进程处理int 3事件。 3、尝试让进程自己处理int 3事件(参考C+的异常以及Windows Structured Exception Handling知识)。 4、如果2、3情况都没处理int 3事件,则系统弹出异常对话框,通知用户进程发生了异常(此时用户可以使用调试器再来处理int 3事件转入2)。 第一类中断一般直接由系统处理,然后可能再分发给需要处理的用户进程。 第二类中断一般处理顺序为 1 - 3 - 2 - 4 .i,4,调试断点,调试断点一般是通过int 3指令实现的。 调试器设置断点原理(以VC调试器为例): 调试器首先找到被调试进程需要设置断点的指令地址(调试版本根据源代码设置的断点也会被转化为实际的指令地址),然后把该地址的1byte数据记录到一张对应表里,接着把这1byte 改写为0 xCC (即int 3指令码)。这样当程序被调试运行的时候,在断点位置的指令其实就是int 3指令,参照上一节的int 3中断事件处理过程,就可以明白调试器捕获断点的工作机理。取消断点时则把对应表里记录的1byte回写到被调试进程。 常用跟踪相关动作都是通过断点方式实现的 对于step to、 step over、step in、step out等调试器都是通过在要运行的下一个地址处先设置一个临时断点,然后调试运行程序来实现的。其它断点实现类似。 程序主动调试断点,ASSERT宏,ASSERT(false)即等效为一条int 3指令。 理解和使用条件断点、单次断点、固定次数断点等。 怎么在动态库(静态load和动态load)里设置断点, VC的Additional DLLs选项。 怎样在模板代码、内联函数、静态库代码里设置断点。(调试器问题,怎样在不能设置断点的代码位置设置断点)。 带调试信息模块和不带调试信息模块共存情况的调试方法,如VB、浏览器、Media Player使用我们需要调试的.ocx、.dll文件等。,5,常见调试器功能 - 具体参考VC,gdb等调试器的用户手则,查看和修改变量,监视变量 查看和修改内存,监视内存 查看和修改寄存器,监视寄存器 全局变量写监视 Call Stack(调用堆栈)的查看 更改指令指针寄存器EIP,实现调试时强行跳转(VC 的 Set Next Statement命令同此) 查看源码对应的汇编指令/机器码,6,源代码级别的主动调试手段,编译时刻防御性编程 - C+契约 (contract) 1、静态assert(编译时刻断言) - STATIC_ASSERT(), must_have_base() 2、一些有用的静态判断:bool IS_INT_TYPE(T)、IS_SIGNED_TYPE(T)、GET_INT_MAX_VALUE(T),见 npdebug.h“ 调试时刻防御性编程 宏ASSERT()、VERIFY()、TRACE()等。 MFC的AfxIsValidAddress()、AfxIsValidString()等。 1、程序应该大量使用ASSERT()宏,保证ASSERT()覆盖没有正常处理的所有程序逻辑分支。 2、所有没有完成的函数和逻辑分支应该写上ASSERT(false)以防止以后遗忘。 运行/发布时刻防御性编程 即程序的各种边界、容错、健壮性处理等。,7,C+异常和win32的SEH,什么情况下建议使用异常 a、当使用第三方提供的库,调用该库接口的代码需要放在异常块里面 (对于第三方库内部有独立线程或独立进程时,目前我还没想到好的办法增强程序健壮性) 。 b、构造函数可能失败的情况必须使用异常。 c、在使用异常可以大大简化程序逻辑的地方也可以使用异常。 d、内存分配可能失败的地方。 异常不可能全面代替错误处理。 不可使用异常来做一般的逻辑控制。 宏NP_BEGIN_CATCH_ALL()和NP_END_CATCH_ALL(),8,常见程序问题调试,内存泄漏 内存溢出/越界 多线程死锁 发布版本的调试 分析只在发布版本才会出现的问题 多平台调试,9,内存泄漏,尽量减少对new和delete,malloc和free的使用,尽量使用C+的自动对象,如std:string, std:vector, class CAutoPtr, class CAutoObj等。 检查低级错误,通查程序里面的new、delete、malloc、free等内存操作,delete和delete是否混用 如果对象有引用计数,查看计数是否有问题。可以使用调试器分析是谁在申请内存而没释放,VC里面可以直接在C/C+运行库源代码里面设断点,其它平台通过重载全局的new、delete或者使用hook技术钩住malloc、free后,再在重载/钩子函数里设断点。 用VC自带的内存检测机制(调试运行程序,正常退出后检查内存信息)。使用Visual Leak Detector。 其它方法,如打印、程序折半法等。,10,内存溢出/越界,得到写越界/出错的内存地址(分为全局heap内存和函数局部stack内存),并监控该内存的内容,接着单步执行程序,找到引起该内存变化的语句,此语句就是导致内存越界的直接原因,然后再深入分析,找出真正bug。,11,多线程死锁,理解程序发生死锁的机理。 建议程序里面的线程同步对象全部使用 npsync.h、ILocker.h、NPRWLock.h等代码库里面的函数,struct tagOSMutex:lockedThreadID,即专为解决死锁而设计。 程序发生死锁后利用调试器的线程切换和堆栈查看能力配合lockedThreadID信息,一般来说可以很快找到死锁原因。 如果死锁实在不能避免,建议改造程序逻辑,使用TryLock、SendTimeout等方式。,12,发布版本的调试,使用map文件。 使用手工插入软断点 int 3,直接查看汇编指令。 查看程序CPU、内存、各种句柄使用情况(windows的任务管理器,linux top命令)。 原始方式:打印,printf()、OutputDebugString(),13,分析只在发布版本才会出现的问题A,首先需要理解发布版本和调试版本的不同。 发布版本没有任何调试相关代码,检查是否有错用VERIFY为ASSERT的地方。 发布版本一般的内部不会有初始化动作,而在调试版本,编译器为了便于调试,一般会对内存做初始化。 如VC在调试版本会用0 xCC初始化所有自动变量,用0 xCD填充new出来的内存,用0 xDD填充delete的内存,用0 xFD填充受保护的内存(动态分配内存的前后地址),以上值都是比较大的奇数,这样便于查错。,14,分析只在发布版本才会出现的问题B,发布版本会优化掉一些不必要的操作和变量。 如优化掉一些局部变量就会引发一些只会在发布版本发生的错误,如:【理解x86体系的CPU的堆栈地址是递减的,著名的c语言buffer溢出攻击即是基于此理】 int a; char ch4; /* */ 变量a没有使用或者只是在调试版本使用,当对ch发生向后越界操作时(小于4bytes的越界),在调试版本因为有变量a,不会产生错误,但发布版本int a可能被优化掉,则会引发堆栈错误。 检查有使用#ifdef _DEUBG的地方是否会导致调试版本和发布版本有逻辑差异。 也有可能因为使用系统库的不同,如MFC库,引发一些差异性错误。,15,多平台调试,若程序不是特别平台相关,应尽量让程序可以在多个平台下编译运行,比如在linux平台不易查的问题,可以到win32平台下来查。尽量使用标准C库、stl以及codelib里面的跨平台库。,16,性能分析和优化A,利用x86的RDTSC指令进行精确的时间统计(在多核CPU系统下使用该指令需要小心) 。 对程序进行时间复杂度统计。 利用编译器提供的统计功能,如gcc的GPROF(参考Makefile)。 使用调试器配合软断点(int 3)查看发布版本的汇编代码,了解代码在发布版本里对应的实际执行指令。 优化,找到关键问题所在,记得二八原则,即程序80%的时间在执行20%的代码。写一个模块(函数、类)的时候,时刻想到是可读性重要,还是性能重要。 分清楚什么时候该用ASSERT,什么时候该用错误处理逻辑,常见的做法是在最底层函数使用ASSERT声明所有的非法情况,在上层函数使用错误逻辑处理,这样既保证了正确性,也获得了发布版本的效率。例:在TYPE* CAutoObj:operator - () 对对象是否合法的ASSERT检测,为了效率,此处显然不应该用错误处理(非法时返回NULL)。,17,性能分析和优化B,没必要在某些问题上耗费我们的时间,现代编译器对于有些优化比你做的更好,如:a/2 - a1,没必要把整数乘法变成位移,破坏了程序可读性,这件事情编译器会帮你做。时刻谨记编译时刻常量(C+摸板元编程)是不会耗费任何执行时刻开销的,比如定义 #define XXX (12) 就比 #define XXX 0 x8可读性好。 尽量减少大数据拷贝动作,在读磁盘和内存缓存之间做权衡。减少网络访问次数和传输的数据量。 怎么节约内存和避免内存碎片【在服务器程序和内存受限系统中这是个重要问题】 1、内存池、重载new和delete,class SameSizeMemMgr。 2、在堆栈够用的情况下尽量使用堆栈内存,即尽量使用局部对象这也有利于编译器优化。如当一个数值的长度是常数编译时刻确定的数,则一般使用局部数组。推荐大家尽量使用class CSmartBuf和class CSmartArray来定义局部数组对象。 3、尽量减少new和delete的使用,建议使用自动对象包容摸板class CAutoObj。,18,C/C+语言的一些陷阱A,整数 1、回环问题,怎样用tick统计时间长度; 2、扩展问题,如16位扩展到32; 3、位移问题,如: int32 33不是我们想象的0,而是与int32 1等价,因为c/c+编译器为了效率直接使用了硬件移位,而很多硬件指令的位移就是这么做的。另:在32位平台下VC和gcc实现的INT64 对于 int64 65结果为0 而不是等价于 int64 1。 宏,别忘了在宏里面加括号,如:#define XXX(a) a10 则必
收藏 下载该资源
网站客服QQ:2055934822
金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号