1.5 VC相关开发辅助工具
VC6比起VS2005、VS2008之类的开发工具显得小巧轻便,非常适合入门学习,对于真正的开发也毫不逊色。这里介绍VC6开发环境下的两个工具,一个是比较简单且需要经常使用的“Error Lookup”,另一个是集成在VC 中的调试器(前面介绍的SPY++也可以通过VC6的“ToolS”菜单栏找到)。除了这两个VC 提供的工具外,还会介绍另外一个与Error Lookup工具相似的工具,即Windows Error Lookup Tool。
1.5.1 Error Lookup 工具的使用
Error Lookup 工具可以在VC6 的“ToolS”菜单中找到,它可以对GetLastError()函数提供的出错代码进行解释,解释为可以理解的文字描述。下面通过一个非常简单的程序来解释该工具的使用。
例子代码如下:
#include <windows.h>
#include <stdio.h>
int main()
{
HANDLE hFile=CreateFile("c:\\test.txt", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if ( hFile == INVALID_HANDLE_VALUE )
{
printf("Err Code=%d\r\n", GetLastError());
}
return 0;
}
这段代码非常短小,主要是通过CreateFile()函数打开一个已经存在的文件,但是这里传递给函数的第1 个参数“c:\\test.txt”是一个不存在的文件,那么CreateFile()对c:\\test.txt 文件的打开必然会错误。当打开错误时,程序调用GetLastError()函数会得到一个错误码,并通过printf()进行输出。
编译运行这个程序,看到命令行中输出字符串“Err Code = 2”,说明GetLastError()函数得到的错误码为“2”。有了这个错误码,通过VC6 的“ToolS”菜单打开“Error Lookup”工具,在“Value”处输入“2”,然后单击“Look up”按钮,就可以看到错误码的解释为“系统找不到指定的文件”,如图1-17所示。
图1-17 Error Lookup 工具
在平时写程序的时候,要养成对函数的返回值进行判断的习惯。在编写程序的时候,当调用CreateFile()函数时,指定文件的参数可能是由用户提供的。而当用户指定的文件不存在时,同样会报错。在代码中调用FormatMessage()函数可以将GetLastError()函数的错误码转换为错误描述。(提示:这里只是说明在代码中如何将GetLastError()的错误码转换为错误描述,建议在真正写程序时自行对用户的输入进行判断过滤,以保证程序的健壮性。)
1.5.2 Windows Error Lookup Tool 工具的使用
Windows Error Lookup Tool 工具是第三方的Windows 错误码查看工具。该工具可以查看的错误码的类型有4 类,分别是Win32、HRESULT、NTSTATUS、STOP。随着Windows Error Lookup Tool工具版本的更新,支持的错误码的数量也会不断增多。它相当于一个功能更强大的Error Lookup 的增强版工具。
同样,将错误代码“2”输入该工具的编辑框中,可以看到给出的提示也是“系统找不到指定的文件”。该错误码的类型为“Win32”类型,此类型属于Win32 API 定义的错误代码。除了Win32的错误码外,这里将编写另外一个程序例子来测试该软件。代码如下:
#include <stdio.h>
int main()
{
int*p=NULL;
*p = 3;
return 0;
}
该代码在VC6 下编辑完成后按F5 调试运行,当程序执行到*p = 3;时,程序会报错,如图1-18所示。
图1-18 错误代码为0xC0000005
调试提示的错误码为0xC0000005,将该错误码复制到Windows Error Lookup Tool 中查看,如图1-19所示。
图1-19 错误类型为STATUS_ACCESS_VIOLATION
在图1-19中,错误的定义为STATUS_ACCESS_VIOLATION,意思是访问违例。在例子代码中对0地址进行了赋值,而0地址是禁止访问的地址,因此提示为访问内存违例。目前Windows Error Lookup Tool3.0.6 版本没有对0xC0000005 的错误码给出正确的描述,但是对其他绝大部分错误码都能给出正确的错误描述。(提示:对于指针的赋值,一定要检查指针的有效性。在指针进行定义和指针指向空间释放时,一定要将其赋值为NULL。这样,当程序出错时,可以较容易地找到代码的错误位置。)
1.5.3 VC6 调试工具介绍
在编写代码的过程中,经常需要查找逻辑上的问题,或者是查找一些原因不明的问题。在这种情况下,就需要使用调试工具对编写的代码进行调试,以便能够找到代码中的问题。
1.调试器
调试的一般过程是让程序在调试的状态下运行。什么是调试状态呢?其实很简单,就是让程序在调试器的控制下运行。调试器可以对程序做多方面的控制,这里举几个简单的方面:
(1)调试器对程序设置断点,使程序产生中断从而停止下来;
(2)调试器可以使程序进行单步执行,即执行一条语句(指令)就停下来;
(3)调试器可以让程序运行到光标指定的位置;
(4)调试器在程序处于中断的情况下可以查看程序的各种执行状态,查看变量的当前值、内存当前的布局、当前的调用栈情况。
对于调试器的诸多功能,无法全面介绍,各种使用技巧及方法需要读者慢慢体会。下面的内容将针对上面的介绍来说明VC6中提供的调试器的使用。
2.被调试程序的代码
调试器具有的功能在前面已经进行了说明。前面介绍调试器的功能不单单针对VC6提供的调试器,几乎任何调试器都支持以上功能,而且功能远不止如此。下面举例介绍说明VC6的调试器。
首先新建一个VC6的控制台应用程序,输入如下代码:
#include <iostream.h>
int main(int argc, char* argv[])
{
//定义3个整型的指针变量
int *p = NULL; // 32位的整型变量指针
__int64 *q = NULL; // 64位的整型变量指针
int *m = NULL; // 32位的整型变量指针
//使用new分配一个整型的内存空间
//用指针变量p指向该内存空间
p = new int;
if ( p == NULL )
{
return -1;
}
//为指针变量p指向的内存空间赋值
*p = 0x11223344;
//q和m操作同p
q = new __int64;
if ( q == NULL )
{
return -1;
}
*q = 0x1122334455667788;
m = new int;
if ( m == NULL )
{
return -1;
}
*m = 0x11223344;
//释放3个变量指向的地址空间
//释放顺序依次是q、m、p
delete q;
q = NULL;
delete m;
m = NULL;
delete p;
p = NULL;
return 0;
}
写完该程序后,按 F7 键进行编译连接,生成可执行文件。上面的步骤属于代码编辑、编译、连接的过程。接下来要完成的工作是对这段源代码进行调试,目的是熟悉VC6的调试器,及Debug编译方式下是如何对“堆”空间进行管理的。
说明:堆空间是在程序运行时由程序员自己申请的空间,该空间同样需要程序员自己进行释放。在C++语言中,使用new申请堆空间,按Delete键可以对堆空间进行释放。C语言中的malloc()和free()函数也是申请和释放堆空间的函数。与“堆”空间对应的是“栈”空间,栈空间是由系统进行维护的空间。局部变量和函数的参数使用的都是栈空间,栈空间的分配和回收是由系统自动进行维护的。这里的“堆”与数据结构中的“堆排序”没有任何关系。
3.认识调试窗口
在编辑完以上的代码后,按F10键让程序处于调试状态,开始对程序的调试,程序的窗口界面如图1-20所示。
图1-20 VC 的调试界面
VC的调试界面分为5个区域,(从左到右、从上到下)依次是调试工作区、寄存器窗口、调用栈窗口、监视窗口和内存窗口。除了调试工作区外,其余几个窗口都不是必需的。根据环境的不同,不是每个VC6在调试状态下都会出现这些窗口。除了这几个窗口外,还有其他关于调试方面的窗口。各种调试窗口的打开方式可以通过菜单进行,如图1-21所示。
图1-21 打开调试窗口的菜单
VC6的调试环境提供了6个调试窗口,均是常用的调试窗口。调试窗口的使用非常容易,这里不做过多的介绍。
程序在进入调试状态后,不可能始终通过单步方式让程序一步一步执行。调试器提供了多种调试运行方式,通过调试器控制可以使程序按照不同的方式运行。VC6提供了几种调试运行的方式,如图1-22所示。
图1-22 调试菜单
图1-22中的4种运行方式分别是:
Step Into:这种方式称为单步步入方式,快捷键是F11 键。单步步入的意思是当单步调试时,遇到函数调用时会进入被调用的函数体内。
Step Over:这种方式称为单步步过方式,快捷键是F10 键。单步步过的意思是当单步调试时,遇到函数调用时不会进入被调用的函数体内。
Step Out:这种方式称为执行到函数返回处。当调试进入某个函数时,这个函数又不是调试的关键函数,可以通过该方式快速返回。
Run to Cursor:这种方式称为执行到光标处。当调试时明确知道要调试的地方时,可以使程序运行至光标指定的位置。
除了上面几个调试命令外,再介绍3个调试的命令,分别是F9、F5和F7键。F9键是在光标指定的位置设置断点,当程序在调试状态下运行时遇到断点,会产生中断(中断后可以观察变量值,某块内存中的内容);F5 键使程序进入调试状态运行,如果代码中有断点,则会在断点处产生中断,如果没有断点,程序运行完自动结束调试状态;F7键是结束调试状态下运行的程序。
在调试程序时,尤其是调试代码量非常大的程序时,往往不可能通过单步执行一直来进行调试。通常情况是在某个或某几个关键的位置设置断点,然后让程序处于调试运行,当运行到断点处,程序会产生中断,这时再通过单步调试方法调试重要的代码部分,观察变量、内存、调用栈等数据的实时变化情况。
4.调试程序
前面的准备工作都已经完成了,接下来就来调试上面编辑的代码。按F10键,让程序处于调试状态,在监视窗口(Alt+F3组合键显示的Watch窗口)添加要监视的变量,分别是p、q、m、&p、&q、&m。当前调试的光标在main()函数的第一个花括号处,按F10键单步执行一步观察监视窗口,如图1-23所示。
图1-23 Watch 窗口的说明
观察如图1-23所示的Watch窗口,通过&p、&q和&m可以看出,3个指针变量p、q和m已经分配了变量的空间,分别是0x0012ff7c、0x0012ff78、0x0012ff74。从这里可以看出,在主函数中先定义的变量的地址(局部变量使用的是栈地址)要大于后定义的变量的地址。由于在Win32系统下指针变量所占用的空间大小为4字节,通过3个地址值可以看出,3个变量的地址按照定义顺序依次紧挨。变量p、q和m的值为0xcccccccc,这是VC6 Debug 编译方式下默认对局部变量初始化的值。
单步执行到p = new int;代码处,观察监视窗口,这时可以看到3 个变量的值为0,因为3个变量经过初始化后值都被赋为NULL。
在if( p == NULL )代码处按F10 键,观察p 指向地址的值,如图1-24 所示。在VC6的Debug编译方式下,未进行赋值的堆空间的值为0xCDCDCDCD。
图1-24 未赋值的堆空间的值为0xCDCDCDCD
按F10 键单步到q = new __int64;代码处,观察监视窗口和内存窗口(内存窗口调整为每行显示16字节),如图1-25所示。
图1-25 通过监视窗口的地址观察内存窗口
在监视窗口中,将&p、&q 和&m进行修改,修改为(int *)&p、(__int *)&q 和(int *)&m。这里简单说明一下,指针变量p的地址为0x0012ff7c,p指向的地址为0x00382e50,p指向的地址中的值为0x11223344。观察内存窗口,在0x00382e50 处保存的值为44 33 22 11(相当于0x11223344。关于为什么顺序是反的,在后面的章节中会给出解释)。
注意:有些C语言的书中说道,指针就是地址。这样的说法是不严密的,准确来说,指针是有类型的地址。“*”操作需要根据指针的类型来进行取值。对于一个指针,要了解其4个方面,分别是指针的类型、指针的地址、指针指向的地址和指针指向地址的值。如果对这里的解释不明白,请复习C语言关于介绍指针的部分,这里不对C语言进行过多的介绍。
按F10键单步执行到delete q;代码处,将p指向的地址减0x20字节,即0x00382e50 – 0x20 = 0x00382e30,然后在内存窗口中观察,如图1-26 所示。
图1-26 内存窗口
现在来分析图1-26中的内容,通过监视窗口可以看出p指向的空间为0x00382e50,q指向的空间为0x00382e98,m指向的空间为0x00382ee0。这3个变量指向的空间比较近。再来观察内存窗口, 0x00382e30 地址处的值为“98 07 38 00 78 2e 38 00”,这里是两个地址,分别是0x00380798 和0x00382e78;0x00382e78 地址处的值为“30 2e 38 00 c0 2e 38 00”,这里也是两个地址,分别是0x00382e30和0x00382ec0。0x00382e30是不是看着比较眼熟?这个值就是内存窗口中第一个地址的位置。0x00382ec0 地址处的值为“78 2e 38 00 00 00 00 00”,这里同样是两个地址,分别是0x00382e78和0x00000000。0x00382e78是不是看着比较眼熟?整理一下这几个地址,如图1-27所示。从图1-27中可以看出,使用new申请的堆空间是通过双向链表进行链式管理的。图1-27所示为最后一个节点的0x00000000表示链表的结尾。
明白了链表是链式管理后,接着分析其他相关数据。当使用new申请的空间不再使用时,会使用delete释放空间,那么delete要释放多大的空间呢?堆空间的首地址处是管理双向链表的指针,在首地址偏移 0x10 的位置记录了堆空间的大小,第一个堆空间的首地址是0x00382e30,偏移0x10的位置是0x00382e40,在0x00382e40地址保存的值为4。其余几个用new申请的空间的大小通过这种方式也可以找到。
在堆空间偏移0x18的位置记录堆的一个序号,程序中通过new申请的第1块堆空间的序号为30,第2块为31,第3块为32。
图1-27 堆的链式管理
在图1-26 中,每个数值的前后(对p、q 和m赋的值)都有4个“FD FD FD FD 44 33 22 11 FD FD FD FD”,前后的FD 是用来在调试时检测溢出的。当为指向整型地址的p 变量赋值超过4字节时,就会覆盖数值后面的FD;当调试程序时,通过查看FD的值,就可以观察到赋值溢出了。
关于堆的管理结构就介绍这么多,继续按F10键单步执行,执行到q = NULL;语句处,观察内存窗口,如图1-28所示。
图1-28 释放q 指向的内存后的内存布局
通过图1-28可以看出,释放后的堆空间会被赋值为“EE FE”。观察堆链表的指针的变化,第1块堆的后继链表指针指向了第3块堆,第3块堆的前驱链表指针指向了第1块堆。关于链表的具体操作,需要学习和阅读关于数据结构的知识。
提示:VC 默认提供2种编译方式,分别为 DEBUG 和 RELEASE。以上堆管理方法为DEBUG编译方式,RELEASE编译方式并不是该种管理方法。