windows中的调试
1.事后调试有两个最基本的目标:
(1) 发现程序是在哪里崩溃的
(2) 找出导致程序崩溃的原因
2.按优先选择顺序排列的调试技术:
(1) 使用调试版本进行本地调试
(2) 使用带有调试符号的发布版进行本地调试
(3) 使用调试版本进行远程调试
(4) 使用带有调试符号的发布版本进行远程调试
(5) 使用 Dr.Watson 日志文件进行事后调试
(6) 使用崩溃对话框的信息进行事后调试
3.返回类型为BOOL型的Windows API函数返回的值不一定是0或1,所以在编写带有调试功能的C++程序时,将返回值与TRUE进行比较是一件很冒险的事情,应避免这样做。返回类型为HANDLE的Windows API函数在有错误发生时,通常返回一个空的句柄,或返回INVALID_HANDLE_VALUE(值为-1)。返回类型为LONG或DWORD的Windows API函数则通常返回0或-1。如果一个API函数不可能有错误发生,它就返回VOID。可以通过GetLastError得到相应的错误码。
4.在Visual C++中通过在观察窗口中输入"@ERR, hr",可以监控GetLastError的返回值。
5.如果你希望在错误信息中显示错误码,可以通过FormatMessage API函数将错误码转换到文本格式。
6.永远不要重定位其他程序会使用的第三方DLL
7.优化版本通常不将EBP作为堆栈基址指针使用。这种优化类型称为帧指针省略(FPO)
8.因为优化版本可能不使用堆栈基址指针,如果函数原型声明不一致,就很可能会在函数返回时导致崩溃。
9.使用调试反汇编窗口查看你的源代码如何被转换成汇编代码
10.创建最有用的映射文件,通常使用/MAPINFO:LINES和/MAPINFO:EXPORTS工程选项。将你发布的程序的所有模块的映射文件归档。
11.使用Visual C++名字解析工具(Undname)将混合名字转换到原始名字。
Undname ?RandomException@@YGXHHHH@Z
输出:?RandomException@@YGXHHHH@Z == RandomException
-f选项显示出整个函数原型
Undname -f ?RandomException@@YGXHHHH@Z
输出:?RandomException@@YGXHHHH@Z == void __stdcall RandomException
11.调试版本的运行时刻函数库对内存的分配作了跟踪,并允许用户检查内存泄漏;在刚分配的内存里写上0XCD的字节模式,这有助于发现使用未被初始化数据的错误;在被释放的内存里写上0XCD的字节模式,这有助于发现使用已被释放的内存;在缓冲区的两边分配了四字节的保护数据,并用0XFD的字节模式作初始化,来检查写内存的上溢出和下溢出;在每个内存分配的地方对源代码文件名和行号作了记录,这有助于用户在源代码中对内存分配进行定位。所以,调试版本能发现多种的内存错误。
12.帧指针的省略(FPO)隐藏了函数原型不匹配的错误,这仅在调试版本中函数返回时会导致崩溃。
13.在调试版本中所有的变量都是volatile的。而在发布版本中如果一个变量未设置成volatile,那么便会有一个与优化相关的错误。如果时多线程,那程序出问题的可能性非常大。
15.发布版本中的变量优化问题:
void StackAttack () {
int optimizedOut1,optimizedOut2;
TCHAR bugsText[16], *bugs=_T("This function has bugs!");
_tcscpy(bugsText,bugs);
}
在这个函数中,bugsText缓冲区的长度不能接收bugs字符串。不必要的变量optimizedOut1和optimizedOut2在调试版本中会保护堆栈内容不被破坏,但这些变量在发布版本中会被去掉。导致的结果是,缓冲区的溢出会破坏堆栈的函数返回地址,从而发布版的程序会崩溃,而在调试版中不会。一般的,被优化的变量不会这么明显。
12.最好为你的可执行程序创建调试符号,并将得到的PDB文档存档,即使程序属于发布版本。
13.为程序的某个版本创建调试符号,要对程序所对应的Visual C++项目做如下的设置:
(1) 打开工程设置对话框,在Setting...对话框中选择所需的版本(如:Win32 Release)。
(2) 在工程控制树里,通过单击根节点选择整个工程。
(3) 在C/C++标签里选择General类。在调试信息里,如果是发布版本则选择Program Database,如果是调试版本则选择Program Database for Edit and Continue(注意:编辑继续选项与优化连接不相容,而且它还增大可执行文件的长度,从而不适合于发布版本)。
(4) 在Link标签里选择Debug类。然后选择Debug info和Microsoft format选项。记住不要选择Separate types选项,这样所有的调试信息才会被合并到单独的一个PDB文件中。另外,如果你需要做事后调试的映射文件时,记住要选择Generate Mapfile选项。
(5) 对于发布版本,选择Link标签,在Project Options对话框的最后加上“/OPT:REF”。这个选项使得不被引用的函数和数据不会出现在可执行文件中,从而避免了文件无畏的增大。对于调试版本不要使用这个选项,因为它会关闭增量链接(incremential linking)
(6) 使用Rebuild All命令重新编译整个工程。
注意:如果你发现带有调试符号的可执行文件比不带调试符的可执行文件大许多,很有可能你忘记了加上/OPT:REF链接选项。
14.为了进一步控制调试,要使用反汇编代码窗口进行代码的调试
15.最好不要使用Separate types选项,除非你要在一个非常慢的计算机上编连非常大的工程。
16.可以在观察窗口中显示TIB(thread information block)结构,使用@TIB,并在程序中添加如下代码:
#ifdef _DEBUG
#include "tib.h"
PTIB pTIB
#endif
在观察窗口中通过pTIB=@TIB来查看TIB内容。
17.使用Autoexp.dat
18.在Windows2000里设置系统调用断点:
(1) 确定包含API函数的模块。
findstr MessageBox win32api.csv
(2) 确定模块对应的调试符号已装载
(3) 确定真正的函数名
dumpbin -symbols user32.dbg | findstr MessageBox
返回“_MessageBoxA@16”。如果调试符号没有被装载,使用命令:
dumpbin -exports user32.dll | findstr MessageBox
返回“MessageBoxA”,注意,“MessageBox”只会被预处理器看到,它会将名字转换为“MessageBoxA”或“MessageBoxW”,“A”代表ANSI,而“W”代表宽字符或Unicode。
(4) 在断点对话框里设置断点。如果调试符号被装载了,输入
{,,user32.dll}_MessageBoxA@16
如果调试符号没有被装载,输入
{,,user32.dll}MessageBoxA
如果调试符号没有被装载,你还需要在选项对话框的Debug标签里设置Load COFF&Export选项。这个选项允许你在没有调试符号的情况下,在输出函数上设置断点。
注:如果没有findstr.exe工具,可以使用Visual C++ 的 Find in Files命令。
19.如果返回值不大于32位,可在观察窗口键入“@EAX”查看,如果返回值长为64位,其低32位会放在EAX中,高32位放在EDX中。如果返回值大于64位,会在EAX中放入指向返回值的指针,可以通过在观察窗口中进行类型转换,例如:若返回一个CRect,则可以键入“(CRect*)@EAX”显示结果,或在内存窗口的Address栏中直接键入EAX查看返回值。
20.使用API函数GetAsyncKeyState帮你调试WM_MOUSEMOVE消息。
21.使用Spy++调试与消息有关的问题。
22.使用回调帮助你调试Windows代码:回调允许你进入Windows看它在做些什么。
23.PostMessage和SendMessage的区别: PostMessage只负责将消息放到消息队列中,不确定何时及是否处理SendMessage要等到受到消息处理的返回码(DWord类型)后才继续
24.可以使用X86的内存对齐规则来确定一个指针是否有效。堆栈、堆的指针都是双字对齐的,所以,它们的最后一位应该是十六进制的0、4、8、C。当X86指令可以是任何大小,所以指令指针的最后一位可以是任何数字。函数是16字节对齐,所以函数指针的最后一位应该总是零。
25.要发现对已被释放的内存的访问操作,一定要将被释放的指针置为空。