Inside VC debug
本文内容来源于我在2004年给所在部门的新员工做的一次内部培训。在对新员工的培训过程中,发现对于新员工来说,在进入工作岗位后,关注编程的技巧比较多,而对于VC/Windows环境下的程序调试,以及相关工具的使用掌握的不好。从我自己学习的过程来看,调试和工具的使用主要靠长期的积累和摸索,相关的资料非常少,部分调试技术,对于工作多年的老员工来说,可能也从来没有接触过。下面我就将自己在日常工作中使用的一些调试经验作一些介绍,希望对大家有所帮助。
一、 调试
1. 基本技巧
1) 断点的设置与跟踪
a) 普通断点设置 F9
b) 断点进入条件(Ctrl+B)
2) 跟踪
Step Into 进入函数、模块调试
Step Over 不进入函数调试
Step Out 从函数中返回
Run to Cursor 运行到鼠标所在行
3) 察看数据
a) Watch
可以自由添加需要查看的变量
b) Call Stack
运行堆栈,对出错(红叉)时的错误定位非常有效
c) Memory
内存查看器。主要用于查看内存块内的数据。图中查看了b变量在内存中的值(第一个字节0A为变量b的值,即10)
d) Variables
变量查看器。自动显示当前代码用到的变量的值。
e) Registers
寄存器查看器。用于查看寄存器内的数据值。用于底层代码调试。
f) Disassembly
查看汇编代码。
下面为函数aa()的汇编代码
8: void aa()
9: {
00401000 push ebp
00401001 mov ebp,esp
00401003 sub esp,48h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-48h]
0040100C mov ecx,12h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
10: for(int i=0;i<100000;i++)
00401018 mov dword ptr [ebp-4],0
0040101F jmp aa+2Ah (0040102a)
00401021 mov eax,dword ptr [ebp-4]
00401024 add eax,1
00401027 mov dword ptr [ebp-4],eax
0040102A cmp dword ptr [ebp-4],186A0h
00401031 jge aa+3Bh (0040103b)
11: {
12: int b=i;
00401033 mov ecx,dword ptr [ebp-4]
00401036 mov dword ptr [b],ecx
13: }
00401039 jmp aa+21h (00401021)
14: }
4) 高级技巧
a) 伪寄存器
可以直接在Watch窗口中查看,特别是前2个,可以大大减少GetLastError的使用,免于调试过程中反复改动代码。
@hr 最后出错编号。等于GetLastError()
@hr,err 最后出错编号的文字描述
@EIP 指令寄存器
@寄存器名称 对应的寄存器
b) 逆向运行
X86 CPU运行时,当前运行代码所对应的地址保存于IP寄存器,32位CPU保存在EIP,所以,我们可以理论上可以依靠修改EIP的值来让我们的代码跳转到任意代码行(考虑到函数调用时的参数需要压栈,如果要跨函数恢复到完全一样运行环境,比较困难,所以在本函数体内跳转比较合适,超出函数范围就不建议使用本技巧了)
实例:
程序运行情况如下图。
1. 当前运行到第23行代码a=2。
2. 我们可以在编辑区和Watch的@EIP伪寄存器中看到23行代码对应的内存地址是0x401096。
3. 现在我们想让代码跳转到0x0040108F,让程序再运行一次22行的代码。那我们只需要将@EIP值改为0x0040108F。
4.在下图中,我们可以看到代码被反向运行了。此技巧主要用于反复运行同一行代码或反复调用同一个函数。
c) 性能测试(Profilling)
可以统计每个函数在运行期间被调用了多少次,占用了多少CPU百分比。
但是此功能在VC6中不是很稳定,经常无法运行。另外,BoundsCheck的最新版本中,增加了Performance测试,可以进行函数和代码行级的性能统计。所以推荐使用BoundsCheck来进行测试。(7.0以上版本才包含此功能,但是由于版权问题,不能大范围使用,可以使用专用电脑来进行性能统计测试)
d) 远程调试(Debugger remote connection)
需要设置:
1.对方IP
2.附加DLL
要求2台调试机器的操作系统(包括补丁)版本完全一样。否则无法调试。一般用于调试图形程序等调试器(VC)会对代码运行产生影响的程序。
2. 错误定位
1) 基本技巧
开发机:运行碰到错误。
调试运行。碰到红叉后根据Stack信息定位到调试代码具体位置。
2) 高级技巧
测试机、用户机器:运行错误。
有代码行数时:直接调出源代码查看。
只有出错内存地址: 可以使用.map文件定位错误函数即行数。在大型程序中,往往不能提供完备的.map文件,无法定位到代码行数或函数。这个时候可以使用ProcessInfo工具查看程序模块映射关系来推断出错模块。
例如:在ATNotes.exe运行过程中出错,出错地址为 7C920100,则根据上图,可以发现出错地址位于ntdll.dll模块中,因为ntdll.dll的基地址(BaseAddr)等于7C920000,模块所占内存大小为94000,说明7C920000~7D9B400为ntdll.dll的代码段。所以可以定位此模块出错。
只有出错框,没有显示是哪个程序出错。有时候会有多个程序同时运行,当出错时,如果出错框没有显示程序名称,很难定位到底哪个程序出错了。
使用Spy++查看对话框信息,获得进程PID
打开进程管理器,查看进程和PID对应关系
可以发现出错对话框属于TestESP(8B0 = 2224)
3. 工具
1) BoundsCheck
主要提供详细的调试信息。常用来检查内存、资源泄漏。
2) Process Explorer
主要用来查看进程详细信息,如进程加载了哪些DLL等。
3) VC Tools
VC所带的工具,常用的有:
SPY++:查看窗口信息
Dependency:查看DLL接口
OleView:查看COM组件信息
TestContainer:用于调试控件
ErrorLookup: 用于查看出错代码对应的错误说明
4) DebugView
用于查看TRACE输出的调试信息,目前被软件开发部门大量使用。
5) SoftIce/Ollydbg
这2个工具功能非常强大,在调试中,主要用于已经在运行的程序调试。可以在指定的函数入口设置断点(包括系统API)。当函数被调用时,就可以进入断点开始汇编级的跟踪。
举例:在一次程序故障中,发现某台计算机中的程序出现运行不正常情况,但是出现概率极小,同时,在不稳定时,不允许退出程序进行代码调试,最后,经过分析,在开发机上大致定位了出错可能的范围,然后在出错机器上使用Ollydbg,定位到了可能出错的代码区,加入断点,跟踪运行期的汇编代码,最后查明了故障原因。
OllyDbg
本节涉及到的工具可以在我的ftp下载:ftp://10.10.20.60/tools
二、 单元测试
1. DLL、OCX模块介绍
1) DLL的类型:
常规DLL:生成时自动生成CwinApp类,帮助管理DLL
扩展DLL:没有生成CwinApp,DLL入口主要依靠DLLMain的DLL_PROCESS_ATTACH
和DLL_PROCESS_DETACH消息
2) DLL的常用加载方法:
静态加载
VC工程设置加载(命令行加载)
代码加载
#pragma comment (lib,"vfprojectd.lib")
动态加载
调用ci_r_IsServer()函数
HINSTANCE hDLLHandle = AfxLoadLibrary (“vfprojectd.dll”)
if(hDLLHandle)
{
typedef BOOL (*CI_R_ISSERVER)(void);
CI_R_ISSERVER ci_r_IsServer = (CI_R_ISSERVER)GetProcAddress(hTempDll,"ci_r_IsServer");
if(ci_r_IsServer)
{
m_bSelfDS = ci_r_IsServer();
}
FreeLibrary(hDLLHandle);
}
3) OCX常用调试方法:
Ocx主要使用TestContainer或VB调试,不建议使用VC
2. DLL单元测试举例
模块名称:VFProjectD.dll
头文件:OtherUseDll.h
库文件:VFProjectD.lib
加载方式:静态加载
VFProject共有10个接口函数。
对一个接口的测试,需要包含:边界测试、中值测试、随机数测试、非法数据测试
/*******************************************************************
*
* 函数名称: CTestVFProjectDlg::OnTest1()
* 描 述: test FileDlg()
* 创 建: 何军[2006-4-20 13:15:32]
*
* 返 回: [void] -
*
* 函数参数 :
*
*******************************************************************/
void CTestVFProjectDlg::OnTest1()
{
// open
CString pathn,filen;
FileDlg(TRUE,"pic","pic",pathn,filen);
Trace("Test1.1 Open Dialog:pathname=%s,filename=%s/n",pathn,filen);
// save
FileDlg(FALSE,"pic","pic",pathn,filen);
Trace("Test1.2 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
// 非法参数
FileDlg(TRUE,"","",pathn,filen);
Trace("Test1.3 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
FileDlg(FALSE,"","",pathn,filen);
Trace("Test1.4 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
}
测试结果
一些补充:
Go命令(F5):向下执行程序直到断点,或结束;
Restart命令(Ctrl+Shift+F5):重新开始调试,直到第一个断点或结束;
Stop Debugging(Shift+F5):结束调试工作,返回原工作环境;
Break:停止调试;
Step Into命令(F11):单步向下运行,该命令可以深入到Call调用的函数里,
执行每一步汇编指令,此时可以打开Registers窗口,观察各寄存器的变化;
Step Over命令(F10): 单步状态结束;
Step Out(Shift+F11):从Call函数中跳出来;
Run to Cursor(Ctrl+F10):执行到光标处;
Step Into Specific Function :单步执行特殊的功能;
Exception命令:将列出异常中断程序执行的情况,可以进行修改;
Threads命令:将列出被调试程序中的线程信息,包括线程的标号、名称、地址、
优先级等。
Show Next Statement命令:将当前正在调试的语句标识出来
Quick Watch:快速观察的功能。