windows调试技巧_第1页
windows调试技巧_第2页
windows调试技巧_第3页
windows调试技巧_第4页
windows调试技巧_第5页
已阅读5页,还剩27页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、国际产品中心Windows调试技巧版本管理版本号修订发布时间修订人备注1.0 2007/06/22Silver初始版本1 概要调试Windows程序是一件繁琐而又复杂的事情,掌握必要的调试策略可以使这些工作变得轻松2 调试2.1 Windows调试器类型2.1.1 用户模式(USER-MODE)调试器基于win32 Debugging API,有使用方便的界面,主要用于调试用户模式下的应用程序。这类调试器包括Visual C+调试器、WinDBG、BoundChecker、Borland C+ Builder调试器、NTSD等2.1.2 内核模式(KERNEL-MODE)调试器内核调试器位于C

2、PU和操作系统之间,一旦启动,操作系统也会中止运行,主要用于调试驱动程序或用户模式调试器不易调试的程序。这类调试器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以调试用户模式代码。2.2 准则2.2.1 调试前要求2.2.1.1 编写便于调试的代码1. 少用全局变量 2. 所有变量都要初始化,成员变量在构造函数中初始化 3. 尽量使用const 4. 详尽的注释 2.2.1.2 使用断言使用 Assert(原则:尽量简单)例子: char* strcpy(char* dest,char* source)assert(source!=0);assert(

3、dest!=0);char* returnstring = dest;while(*dest+ = *source+)!= 0);return returnstring; 2.2.1.3 使用跟踪语句使用Tracea)、TRACECString csTest “test”;TRACE(“CString is sn”,csTest);b)、ATLTRACEc)、afxDumpCTime time = CTime:GetCurrentTime();#ifdef _DEBUGafxDump << time << “n”;#endif2.2.1.4 使用异常和返回值用GetLa

4、stError来检测返回值,通过得到错误代码来分析错误原因程序设计时一定要考虑到异常如何处理,当错误发生后,不应简单的报告错误并退出程序,应当尽可能的想办法恢复到出错前的状态或者让程序从头开始运行,并且对于某些错误,应该能够容错,即允许错误的存在,但是程序还是能够正常完成任务。2.2.1.5 编译选项的选择1. 总是使用/W4警告级别 2. 在调试版本里总是使用/GZ编译选项,用来发现在Release版本中才有的错误 3. 没有警告的编译:保证在编译后没有任何警告,但是在消除警告前要进行仔细检查 2.2.2 调试五步曲l 确定错误的存在l 收集错误的信息l 分析错误的信息l 消除错误l 修改的

5、验证2.3 知识补充2.3.1 汇编知识FUNC(参数1,参数2, .参数n)局部变量1局部变量2。局部变量n原则1、 无论函数的调用规范,参数都是从右往左压栈2、 栈是向低地址发展的,栈顶在低地址 -低地址按照以上原则,调用函数FUNC时的栈情况为1、 先把函数最右边的参数N入栈,以此类推压入函数的参数2、 将调用此函数前下一指令位置(也就是此函数执行完后的返回地址)压入栈3、 进入了新的函数内部,有了新地EBP地址把,把外层的EBP地址压入栈,便于返回后可以利用,然后为局部变量预留空间,可能先调整ESP的地址,在把局部变量入栈之所以每调用一次函数,都要更新他的EBP,把前一个的EBP的地址

6、入栈,是为了返回后方便查找调用函数的局部变量等信息,更新新的EBP,对于被调用函数而言也是方便查找本函数内部的局部变量和参数2.3.2 堆栈l 堆栈是一种先进后出的数据结构 , 数据的存取在栈顶进行 , 数据入栈使堆栈向地址减小的方向扩展l 堆栈常用于保存子程序调用和中断响应时的断点以及暂存数据或中间计算结果l 堆栈总是以字为单位存取以下的问题可能在堆栈里发生void function(int a, int b, int c) char buffer15; char buffer210;void main() function(1,2,3);void function(char *str) c

7、har buffer16; strcpy(buffer,str);void main() char large_string256; int i; for( i = 0; i < 255; i+) large_stringi = 'A' function(large_string);这里发生了什么事? 为什么我们得到一个段错误? 答案很简单: strcpy()将*str的内容(larger_string)复制到buffer里, 直到在字符串中碰到一个空字符. 显然, buffer比*str小很多. buffer只有16个字节长, 而我们却试图向里面填入256个字节的内容.

8、 这意味着在buffer之后, 堆栈中250个字节全被覆盖. 包括SFP, RET, 甚至*str!我们已经把large_string全都填成了A. A的十六进制值为0x41. 这意味着现在的返回地址是0x41414141. 这已经在进程的地址空间之外了. 当函数返回时, 程序试图读取返回地址的下一个指令, 此时我们就得到一个段错误2.4 问题的定位2.4.1 Windows调试定位2.4.1.1 Crash对话框2.4.1.2 MAP映射文件首先要求编译代码时选择生成MAP文件,同时为了方便定位错误代码的位置,要求输出的MAP文件里包含行号信息;操作如下例子代码图1运行发生crash:图2地

9、址的含义:crash所在模块的基地址+ offset地址 (在map文件中的RVA)= 汇编指令执行crash语句命令地址 offset地址 = MAP文件中的address + PE头大小从map文件读出此模块的基地址图3图4右边这个地址是汇编指令执行的地址汇编地址 0x004021b0 = 0x00400000(基地址)+ 00001000(PE头大小)+ 0x000011b0 (ADDRESS)根据crash 对话框里offset地址算出对应MAP文件里的address为0x000011d0,根据此值在图4中找最接近的地址,出错的文件为testlendlg.cpp图5根据testlend

10、lg.cpp在map文件里输出的行号信息(图5)找到最接近地址0x000011d0,行号179通过反编译可看出在地址0x004021D0发生crash,是因为EAX的指针为空2.4.1.3 PDB文件在 Visual C+ 6.0 中的使用方法:1. 打开 Visual C+ 6.0 的 Workspace 文件(*.dsw); 2. 进入 Tools 菜单,选择 Options 菜单项 (Tools->Options); 3. 单击 Directoties 标签; 4. 在 “Show directories for”下拉列表中选择 “Executable files”; 5. 将符号

11、文件的路径添加到 “Directories” 路径列表中; 6. 单击  OK 完成; 可以通过执行以下的步骤找到崩溃地址所在的源代码和行号l 将程序工程加载进Visual C+l 使用debug菜单中的step into或者F11快捷键执行你的程序l 显示反汇编窗口,view->debug->disassemblyl 在EDIT菜单中选择GOTO指令,在go to对话框的go to what框里选择address选项l 在Enter Address Expression对话框里输入导致crash的地址,如果反汇编窗口不是活动窗口,这个对话框是不可用的l 单击GOTO按纽

12、,在反汇编窗口中找到崩溃地址;l 要在反汇编中找到源代码位置,在上下文菜单里选择GO TO SOURCE则可以上方法中提到的地址是汇编指令执行的地址,不是crash对话框里OFFSET的地址,可通过以下的图点击,要查看错误报告的技术信息,错误报告内容对话框中的address即为崩溃地址,如下图的崩溃地址为5f8379b92.4.2 Visual C+调试定位所有的表达式都按照上下文操作格式l 位置断点(Location Breakpoint)大家最常用的断点是普通的位置断点,在源程序的某一行按F9就设置了一个位置断点,这种断点系统会自动生成符合上下文的操作,放入Location的breakpo

13、ints里其语法:function,source file,binary module.line如: 如果想在特定的位置断点设置符合某种条件的高级断点,可通过Location-Condition设置 a) 位置断点跳过计数. 功能是执行到断点但不在断点处停止,直到执行完一个特定的次数为止,如:想跳过11次以上断点才停下,可设置断点跳过的次数² 先设置一个普通的位置断点,按F9,² 打开BreadPoints对话框,在Breakpoints:中选中刚才F9设定的断点,单击Condition然后到高级设置里修改位置断点的高级选项,设定想跳过的次数b) 位置断点条件表达式如果想对

14、位置断点设定某种条件才断下来,如当i 为7时断下来,可以在condition里输入表达式的判断l 数据断点(Data Breakpoint)软件调试过程中,有时会发现一些数据会莫名其妙的被修改掉(如一些数组的越界写导致覆盖了另外的变量),找出何处代码导致这块内存被更改是一件棘手的事情(如果没有调试器的帮助)。恰当运用数据断点可以快速帮你定位何时何处这个数据被修改结果输出:szName1何时被修改呢?因为没有明显的修改szName1代码。我们可以首先在A行设置普通断点,F5运行程序,程序停在A行。然后我们再设置一个数据断点。如下图:F5继续运行,程序停在B行,说明B处代码修改了szName1数据

15、断点另外可以先获得变量的地址之后,设置address内存断点进行跟踪l 函数断点语法: function,source file,binary module MyClass:FUNC如果a.dll和b.dll都用到了函数CheckMyMem,而你只想在b.dll里设置断点,那么",CHECKMYMEM.CPP,B.DLL.27 则可通过一下的红圈所示可进行高级设置l 内存断点语法:function,source file,binary module addressl 消息断点如果想在自己类里的WM_PAINT里断下来可以先打开WINCORE.CPP文件在1584行设置一个位置断点,然

16、后在location的advance里设置,先获得myclasee的 this指针然后搭配消息进行设置l 观察窗口的利用(Watch)可以直接在观察窗口输入表达式如: 希望设置内存块断点,可以直接使用表达式_CrtSetBreakAlloc(23)每当表达式被求值时,它的值就会被重新设置 上下文操作符:观察窗口通过使用上下文操作符对表达式的范围进行限定,语法如下 function name , source code file , executable file 可以用*指定当前上下文,可以用下面的语句调用Visual C+运行时函数库_CrtCheckMemory() , , msvctrd

17、.dll_CrtCheckMemory()给出源代码文件可以解决不确定的全局变量,例如在多个文件里定义了静态全局变量ErrorStatus,myfile.cppErrorStatus2.5 常见问题调试2.5.1 普通调试技术l 重定位(REBASE)程序的可执行代码以防止虚拟地址空间冲突,改变基地址l 为程序生成MAP文件。l 为程序创建符号SYMBOLS文件l 将程序的工程、源代码、可执行文件存档l 为用户提供一个提交错误报告的机制2.5.2 Visual C+调试技术l 单步跟踪调试代码时如果越过了想要调试的代码,可通过在想要debug的代码处右键设置Set Next Statement

18、,代码继续跑则会直接跳到你设的这个点l 如果想查看某段代码执行的时间,可在Watch窗口使用伪寄存器l 如果函数没有把返回值赋给一个变量,可通过以下方式查看l 可在watch窗口中键入EAX,如果返回值长为64位,低32位放在EAX,高32位防在EDX中,返回的数据类型也可以在观察窗口使用类型转换,如一个函数返回类型为CRect,在观察窗口可键入(CRect*)EAX,如果windows api调用失败,也可以在观察窗口键入ERR 查看GetLastError()的值,ERR这是个调试器伪寄存器,存放最近的一次错误,可以加上,hr翻译这个错误码l 使用set next statement,并在

19、观察窗口里改变相关变量的值,以调试不太可能执行的代码,set next statement修改指令指针的值到下一条指令,不修改堆栈,不执行代码,不创建或释放任何变量l 可在watch窗口中键入EAX,如果返回值长为64位,低32位放在EAX,高32位防在EDX中,返回的数据类型也可以在观察窗口使用类型转换,如一个函数返回类型为CRect,在观察窗口可键入(CRect*)EAX,如果windows api调用失败,也可以在观察窗口键入ERR 查看GetLastError()的值,ERR这是个调试器伪寄存器,存放最近的一次错误,可以加上,hr翻译这个错误码可2.5.3 Windows调试技术l 如

20、何调试画图代码如果正在画图的窗口被调试窗口覆盖,这个画图窗口就会被使无效,也就是调试器的激活影响了窗口的画图,解决方法是防止窗口的重叠,使用系统所支持的最高分辨率;如果被调试的窗口有WS_EX_TOPMOST属性,在调试版本必须去掉,最好的方法是远程调试这样就不会互相影响了另外GDI会将GDI函数调用累积起来,放在一个批处理文件,然后一次性处理一个批处理文件,而不是一次执行一个调用,这一技术大大提高了画图的效率,但是它使得画图的代码很难调试,因为你看不到每行代码执行的结果,你可以在调试版本关掉批处理#ifdef _DEBUGGDISetBatchLimit(0);#endifl 如何调试鼠标代

21、码调试WM_MOUSEMOVE消息尤其困难,只要鼠标一进入调试窗口就中断,通常我们希望鼠标在特定位置或特定环境才发生中断,解决方法,在MOUSEMOVE处理函数里加入某些键状态的判断,在满足的情况下设置断点l 如何调试与消息有关的代码消息传递机制禁止这些信息在堆栈中出现,很难通过调试器看到接收到消息的整体情况,调试消息最好的方法是用SPY+,在WINDOWS 标签页WINDOW FINDER TOOL选择你要监视的窗口,然后在Message标签页选择你要监视的消息,如果选择默认输出选项,第一栏是行号,第2行使接受消息窗口句柄,第3栏的S表示SendMessage,”p”PostMessageR

22、消息句柄的返回值l 可执行文件在载入时候崩溃如何调试l 可执行文件在退出的时候崩溃如何调试l 一个函数在返回的时候崩溃该如何调试如果一个函数在堆栈中的返回地址被破坏了,导致返回地址破坏的最常见原因是些自动变量越界或者是函数原型不匹配,如果函数的参数不匹配或者调用函数和被调用函数之间的调用规则不匹配,都会造成堆栈的破坏,而且很可能破坏函数的返回地址,在调试版本中不会出现,因为调试版本用堆栈基址指针访问函数的返回地址2.5.4 MFC调试技术最常见的MFC错误l 使用错误的函数原形处理用户定义的消息l 保存了指向临时MFC对象的指针如果你要保存一个指向你没有显示创建的对象的指针,你必须非常小心,任

23、何一个从FromHandle返回的对象都不能跨消息的保存,这样的函数包括了GetDlgItem,GetFocus,GetWindow,GetActiveWindow,FindWindow,GetOwner,GetFont GetMenu,GetDC,为了将此类函数的返回对象转换为永久对象,首先必须分配一个对象,然后使用函数把对象和窗口句柄联系起来l 不正确的创建或销毁CFrameWnd 和CView派生的对象,CFrameWnd 和CView对象都设计成自清理2.6 技巧2.6.1 内存调试2.6.1.1 内存破坏在越界的时候不会立即造成影响,当越界的地址被再次使用时才会造成影响在调试版本里如

24、果是写上溢,DAMAGE:after block如果是写下溢,DAMAGE:before block只有在内存破坏发生在数据区的前后4个字节才会出现以上的提示错误,如果溢出超过了保护字节,对内存的写越界不会被发现l 访问已被释放的内存要发现对已被释放内存的访问,一定要将被释放的指针置为空,当堆里的内存被释放后,会被0XDD填充,在任何windows版本里都不会导致非法内存访问,因为指针的内存地址是合法地址l 释放未被初始化的指针,释放非堆指针,多次释放指针在调试版本里,释放不是从调试堆里分配的指针会报以下错误在调试版本里,多次释放指针会提示以下错误以下宏作为参数控制内存调试函数的行为Bit f

25、ieldDefaultDescription_CRTDBG_ALLOC_MEM_DFONON: Enable debug heap allocations and use of memory block type identifiers, such as _CLIENT_BLOCK.OFF: Add new allocations to heap's linked list, but set block type to _IGNORE_BLOCK. Can also be combined with any of the heap-frequency check macros._CRT

26、DBG_CHECK_ALWAYS_DFOFFON: Call _CrtCheckMemory at every allocation and deallocation request.OFF: _CrtCheckMemory must be called explicitly. Heap-frequency check macros have no effect when this flag is set._CRTDBG_CHECK_CRT_DFOFFON: Include _CRT_BLOCK types in leak detection and memory state differen

27、ce operations.OFF: Memory used internally by the run-time library is ignored by these operations. Can also be combined with any of the heap-frequency check macros._CRTDBG_DELAY_FREE_MEM_DFOFFON: Keep freed memory blocks in the heap's linked list, assign them the _FREE_BLOCK type, and fill them w

28、ith the byte value 0xDD.OFF: Do not keep freed blocks in the heap's linked list. Can also be combined with any of the heap-frequency check macros._CRTDBG_LEAK_CHECK_DFOFFON: Perform automatic leak checking at program exit through a call to _CrtDumpMemoryLeaks and generate an error report if the

29、application failed to free all the memory it allocated.OFF: Do not automatically perform leak checking at program exit. Can also be combined with any of the heap-frequency check macros.对于比较难的内存破坏问题,使用_CRTDBG_CHECK_ALWAYS_DF,_CRTDBG_DELAY_FREE_MEM_DF阻止内存被真正释放,已经被释放的内存很快被重用,这使得访问已被释放的内存很难被发现2.6.1.2 内存

30、泄露1) 利用调试版本的内存分配和回收函数定位通过修改代码,使用调试版本下重载过的new函数,当内存泄露时在调试信息窗口打印出泄露内存的文件信息做法入下在stdafx.h加入以下定义#define _CRTDBG_MAP_ALLOC#include<stdlib.h>#include<crtdbg.h>#ifdef DEBUG_NEW#undef DEBUG_NEW#endif#define DEBUG_NEW new(_NORMAL_BLOCK,THIS_FILE, _LINE_)测试时发现把以上代码加入到stdafx.h后会出现编译错误,连接了重定义的c函数,原因是

31、有些文件include了atlbase.h,atlbase.h里面include了malloc.h 对报错的函数定义与crtdgb.h里的定义重复了解决方法了在必须include atlbase.h的文件里不要include到以上的头文件;error C2733: second C linkage of overloaded function '_calloc_dbg' not allowed在要检查的cpp文件里加入,这样就会把具体内存泄露的文件信息答应到调试输出窗口#ifdef _DEBUG /如果是mfc程序会自动生成,非mfc程序则要自己copy这段代码#define

32、new DEBUG_NEW#undef THIS_FILEstatic char THIS_FILE = _FILE_;#endif技巧:在使用了new的cpp里加入以上的代码便于定位问题利用此方法定位了以下泄露问题:D:worksilver_EngGameHall2.0_5_intIBD_QQGameHall_VOBIGameHall2.0CommonFactoryMemoryFactory.cpp(133) : 9907 normal block at 0x04C32EA8, 108 bytes long. Data: < X . > 10 00 00 00 00 00 00

33、00 58 00 00 00 B8 2E C3 04 D:worksilver_EngGameHall2.0_5_intIBD_QQGameHall_VOBIGameHall2.0CommonFactoryMemoryFactory.cpp(133) : 9898 normal block at 0x04C228B8, 108 bytes long. Data: < X ( > 10 00 00 00 00 00 00 00 58 00 00 00 C8 28 C2 042) 设置内存分配号断点通过调试信息输出窗口我们可以看到泄露的内存,而在中的为内存分配的块号,通常这个块号不会改

34、边,当然最好总结出不会改变块好的操作顺序,然后就可以通过设置内存块好断点找到泄露的内存是哪里分配的,这个方法比较简单其原理可查看crtdbg.h文件里的实现,当块号等于设置值时会产生一个debugbreak_CRTIMP long _cdecl _CrtSetBreakAlloc( long lNewBreakAlloc ) long lOldBreakAlloc = _crtBreakAlloc; _crtBreakAlloc = lNewBreakAlloc; return lOldBreakAlloc; if (lRequest = _crtBreakAlloc) _CrtDbgBrea

35、k();设置内存分配断点有2个方法,u 修改代码在程序入口处设置分配号设置分配号断点,这个方法的缺陷是只能设置一次,并且需要重编译代码u 在观察窗口里交互式的设置分配号断点不用编译代码,还能设置多个断点,_initterm 呼叫所有的静态构造器3 案例3.1 CPU占用100%登陆大厅以后cpu占用100%使用windbg拉起程序,使用命令!runaway,这条命令显示应用程序每个线程执行用户模式代码时所花的时间这条命令显示的CPU时间是线程在它整个生命期中所花的。我们还需要进一步查看线程所用CPU时间的变化情况,以把这个输出内容与上次的输出内容做个比较,找出CPU时间增长最快的线程通过对比查看到线程b14的生命周期增长最快,说明这个线程就是罪魁祸首查看这

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论