2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 大话调试器(上篇)

大话调试器(上篇)

时间:2022-05-05 21:21:48

相关推荐

大话调试器(上篇)

写程序总要基于一定的假设或者说前提。而错误的出现往往是由于假设错误所造成的,

所以当出现错误时,需要追踪每个片断的上下文。在问题的这个角度上,调试器是对人

的一种解放。

记得刚进校时,很多同学连C与MSVC都无法分清,调试器就别提了。其实我觉得,不会运

用调试器是会吃大亏的。熟练地使用调试器不但能够避免不必要的人力劳动,也可以通

过分析程序执行来获取或验证很多细节知识。

先对调试器大话一篇是有必要的。至少能够知道它到底对程序做了什么,以避免不必要

的麻烦。也让它变得不再那么神秘:)

1、断点

调试器最主要的功能当属控制程序的执行。这个功能是通过硬件断点来完成的,因而需

要CPU的配合。

具体的过程是这样:

(1) 假设调试器需要在程序地址空间Address处中断程序执行,那它就必须在Address处

写入一断点指令“int 3”(0xCC),当然要把被覆盖的字节事先保存起来;

(2) 然后让程序全速执行,直到执行到了Address位置,此时触发3号中断(断点中断),

于是调试器(通过事先接管3号中断)获得控制权,于是顺理成章地获得CPU的执行权;

(3) 当用户处理完毕,需要继续执行时,调试器写回原先被覆盖的指令字节,并打开CPU

的单步执行标志,然后恢复程序的执行;

(4) 程序在执行了一条指令后,由于CPU处于单步状态,所以会自动触发5号中断(单步中

断)。此时调试器(通过事先接管5号中断)再次获得CPU,接下来再在Address处写回中断

指令。

用流程图表示是这样:

----------- ----------- -----------

| 写入断点 | --> | 执行程序 | --INT 3-->| 恢复代码 |--

----------- ----------- ----------- |

-----------------------------------------------------------

| ----------- -------------- -----------

->| 用户操作 | -->| 设置单步标志 | -->| 执行程序 |-INT 5-

----------- -------------- ----------- |

-----------------------------------------------------------

| -----------

->| 写回断点 | --> ......

-----------

这是调试器最基本的工作流程,其它的功能几乎都是基于这种形式。比如:在call指令处

“Step Over”,则需要反汇编器计算出下一条指令的地址,然后通过在那里写入断点,

来完成过程级的单步执行。

2、栈遍历

这也是调试器必须完成的任务,需要编译器的配合。

一般而言,编译器会在每个函数的开头插入这些代码:

push ebp

mov ebp,esp

来建立堆栈帧。在函数执行过程中,这个被压入栈中的值和EBP的值是不会改变的。

这就等于配合了调试器:调试器只要间址ebp+4就得到了调用当前函数的返回地址;同时

间址ebp可以得到在调用函数开头处的类似由“push ebp”压入的ebp。于是令ebp'=[ebp

],则[ebp'+4]即调用“当前函数的调用函数”的函数的返回地址。

具个简单的例子,设A调用了B,B又调用了C,并且在C中进行栈遍历。则此时的堆栈是这

个样子:

ESP----> ---------------

| C的自动变量区 |

EBP----> ---------------

| EBP | <-- B里的EBP;C开头处的 push ebp 造成

---------------

| B的返回地址 | <-- B调用了C

---------------

| 调用C的参数 |

---------------

| B的自动变量区 |

---------------

| EBP | <-- A里的EBP;B开头处的 push ebp 造成

---------------

| A的返回地址 | <-- A调用了B

---------------

| 调用B的参数 |

---------------

| A的自动变量区 |

---------------

把道理搞清楚了之后,就可以编写下述代码来自己进行栈遍历:

int main(int argc,char* argv[])

{

long e;

__asm mov e,ebp; /* 取得EBP */

while (e)

{

printf("%08X/n",*(long*)(e+4)); /* [EBP+4]即返回地址 */

e=*(long*)e; /* [EBP]即调用函数里的EBP */

}

return 0;

}

从这个小程序的运行结果可以看到,main的返回地址是00401129h(不同的编译环境可能会

有不同的值)。这实际上是C运行库在调用main。

实际上,在开启编译器优化功能时,编译器可能会不再为某些函数建立堆栈帧,故堆栈

遍历不再可行。不过可以通过开关,告诉编译器不要忽略堆栈帧。

3、调试符号

调试符号是这样一种东西:它把地址转换为源代码中的行数,或者转换成函数的名称,

或者变量的名称等等;它也能反向把后者转换成前者。符号级调试就是建立在调试符号

基础之上。

显然,调试符号是调试器自明的东西。它不能由调试器产生,只能由编译器在编译时额

外嵌入。而各种编译器产生的调试符号的格式是不同的。比如:MSVC采用PDB格式,而GC

C采用STABS、DWARF、COFF等格式。

PDB(Program Database)是MSVC以及.NET平台上采用的调试符号,它的历史可以追溯到MS

C7那个年代。PDB是一种描述能力很强的格式,它可以包含源代码文件名、源代码行数、

变量地址、过程地址等等很有用的数据;MSVC6及以后的编译器利用PDB来支持“Edit &

Continue”这一花哨的功能:)

透过一个PDB文件,可以对程序有个轮廓上的了解。但PDB格式更新得很频繁,也没有相

应的规格文档,通常的自行访问PDB的办法只有通过WinDBG自带的dbghelp.dll来做小范

围的读取。不过MSVC7以后,MS公布了一套称为DIA的东西,可以很好的读取PDB里的资料

,而且随着PDB格式的更新,DIA也在更新;但这只能在MSVC7以上使用,新版本的MSDN

Library里有介绍。

关于调试符号,以后还会讨论。

上面的三点是每个符号级调试器必备的技术。虽然说原理很简单,但是实际操作起来却

没有那么容易。比如支持多线程调试问题,对动态库的即时处理,对程序的异常处理,

整合用户的操作等等……不过,这些都是别人的工作,我们只要坐享其成就行了:)

从本篇开始,都以MSVC6为讨论对象。

一、调试准备

1、编译器

通过指定"Debug"编译模式:"Build->Set Active Configuration",即可确保生成的程

序支持符号调试。

但这里有一点需要澄清:虽然有"Debug"与"Release"的编译方式,但作为编译器,它是

没有这两者的概念的。"Debug"和"Release"的编译方式只是通过不同的编译器开关组合

来体现。所以对一个"Release"编译的程序进行符号调试也是可行的。

与调试相关的开关,在命令提示符下执行"cl -?"可以查阅到。

2、系统调试符

没有哪个C程序运行起来不需要运行库的支持;与平台相关的程序更需要直接的系统调用

。这就存在个问题:如果在系统过程内出错,那该如何得到系统级的符号,来跟踪在系

统内的调用流程?有两种解决方法。

一种是安装MS与操作系统同时发布的"Diagnosis Tools"里的系统调试符。

系统调试符是在Windows本身被编译时产生的调试符号。里面有很多信息,包括栈遍历时

需要用到的函数地址;还有一些未公开的全局变量(比如GDI32.dll里就有一个很著名的

未公开全局量)和过程等等。配合这些系统调试符,很容易得到系统调用的上下文。

不过系统调试符有个很严重的缺点:每个系统文件对应一个系统调试符,两者的版本必

须完全吻合才能使用。任何的系统更新操作(比如Service Pack或修补漏洞的补丁),都

会使相应的调试符失去作用,除非及时更新到新的调试符。因此,这样对调试很不方便

。建议如果不进行内核调试(此时系统调试符是最好的选择),就不要使用系统调试符。

另一种是通过对系统模块导出表的动态扫描。

这种方法要求调试器在程序还没开始运行,或加载一个动态库时,能够扫描出所有导出

函数的地址,并插入自己的符号表。可见这种方法有点耗时,不过用起来却是最方便的

MSVC支持这项技术:"Tools->Options->Debug",看到左下角"Load COFF & Exports"了

吗?选定它,即可启用。MSVC默认是不选定的(真不知道MS的人是怎么想的……)。

二、调试启动

调试一个应用程序有两种方式。一种是由调试器启动这个程序;另一种是先启动程序,

然后在一定时刻让调试器附加到上面。

后一种方法一般是在应用程序出错时,或不希望重新启动程序时采用。只要通过"Build-

>Start Debug->Attach to Process",即可根据应用程序的PID(进程标识符)选定。

正常的开发中,一般是通过第一种来开始调试:"Build->Start Debug->Go",或者直接

按下F5。

除了F5,还有两种方式启动,一种是"Step Into"(F11),另一种是"Run to

Cursor"(Ctrl+F10)。前者在启动调试器后自动停在main函数的第一条语句处,后者在启

动后直接执行到光标所在语句处中断(急性子----比如我----朋友的好选择哦:))。

三、调试窗口

开始调试后,MSVC的界面会发生改变,用于显示与调试相关的窗口。这些重要的窗口有

:Output、Watch、Call Stack、Memory、Variables、Registers和Disassembly。通过"

Views->Debug Windows"可以显示或隐藏它们。

1、Output(Alt+2)

Output窗口是多用的窗口,不全是显示调试消息的。这里说的是它的Debug标签对应的那

一栏。

它用来显示调试器的反馈消息,或应用程序向调试器主动输出的消息(以后还会提到)。

Output有两种反馈消息值得注意。

其一是通知模块符号的加载情况。如果没有调试符可用,Output里会有类似"no

matching symbolic information found"的文字。这就需要通过"调试准备"里的两种方

法之一添加符号(两种都使用的话,则第一种优先)。如果采用第二种,消息会变成"Load

ed exports for xxx.dll"。

其二是进程结束时的返回值。Output是惟一一个在调试结束后不会被自动关闭的窗口。

它会显示程序进程结束时的返回值(32位无符号整数)。一些工具程序在出错时并不显示

出错信息,而是向操作系统返回一个非零值来指示原因;这种情况下,从调试器的Outpu

t窗口可以看到这个返回值。

2、Watch

Watch窗口是符号调试过程中最重要的窗口。在符号可用的情况下,使用Watch可以自由

察看几乎所有C语言合法表达式的值(这些表达式可以含有标准运算符,也可以含有变量

、函数等等);甚至可以利用Watch窗口进行类型转换或执行一段程序!

可以把Watch窗口想象成一个编译器,只不过它只能处理表达式而已(实际上Delphi的调

试器就直接依赖编译器)。

留意Watch窗口的底部,共有4个标签,也就可以把需要监视的表达式分成四类以方便使

用。

举些例子(不要把引号也输入到Watch里了^^):

"a":显示变量a的值;

"&a":显示a的地址;

"*(unsigned char*)0x400000":显示内存位置400000h处的一个字节的值;

"func()":调用过程func(如果有的话)。

"a=3":把2赋值给a。

Watch里有一些功能很强的特别用法,第三篇专门叙述。

3、Call Stack

"原理"部分已经说明了栈遍历的算法。事实上,栈的显示需要调试符号的参与。Debug方

式编译的程序可以让Call Stack清楚显示调用流程(但涉及到系统内部的地方,则仍需要

按"调试准备"里的两种方法之一告诉调试器如何获得调试符)。

双击某一层,则直接在该层的调用处显示。

Call Stack上的右键菜单有许多很不错的功能。比如在栈的某一层右单击,选择"Run

to Cursor",则会让程序一直执行到该层函数结束对上一层的调用时中断。

4、Memory & Registers

Memory是察看内存的;

Registers是察看寄存器的。

这两个窗口本没有什么好说的。不过有两点请留意:

其一是Memory窗口不但接受一个32位无符号整数作为地址,也可以接受一个表达式作为

地址(仍然可以含有当前可见的变量);不过它对表达式的解释有点不同于Watch窗口:比

如输入"a",那么它从a的地址处显示;如果输入"a+3",那么它先用a的当前值计算a+3,

然后把结果作为地址,从那里显示;

其二是Registers的EAX值是最值得关注的;因为通常情况下函数的返回值是通过EAX寄存

器来传递的。当一个函数执行结束后,想在调试器里察看它的返回值(如果有的话),只

要看EAX即可。

另外,Registers里的标识寄存器是被分解显示的:

OV:溢出标记;UP:方向标记;EI:中断启动标记;PL:符号标记(结果是正还是负);

ZR:清零标记(结果是否是0);AC:辅助进位标记;PE:奇偶标记;CF:进位标记。

5、Disassembly(Alt+8)

MSVC的反汇编窗口是多用的,它可以同时显示源代码,汇编码,机器码。右捷的快捷菜

单里有控制显示的选项。

Disassembly有一个特有的功能:在某代码处右单击,选择"Set Next Statement",可以

直接修改IP,使得此处成为下一条待执行的指令。这就好像goto一样自由。

请注意:该功能只出现在Disassembly窗口的快捷菜单里,其它地方都没有。

四、调试控制

对于一个正在运行的程序,想要立刻让它中断执行以供调试,只需"Build->Break"即可

。多线程的切换问题以后会提到。

MSVC有很多种方式让用户控制程序的执行。比如上述在Disassembly窗口里"goto"就是一

种。除此以外,常见的有"Step Into"、"Step Over"、"Step Out"、"Run to Cursor"等

等。它们都在Build菜单里。

首先需要说明一点,作为调试器,它会根据激活的窗口类型来决定用户操作的意义。对

于源代码窗口,所有操作均在源代码一级完成;对于Disassembly窗口,所有操作均在汇

编码一级完成。

"Step Into"(F11):在第一种意义下是逐语句(这里的语句就是代码中的语句,比如"a=3

;"等);在第二种意义下是逐(CPU)指令执行;

"Step Over"(F10):在第一种意义下是逐函数(比如在源代码里定义的函数)执行;在第

二种意义下,是逐过程执行(实际上就是碰到call时不跟踪进去,而直接执行到call的下

一条指令处中断)。

两者的区别在于遇到函数(过程)时,前者会跟踪进去,而后者会视函数为一条"指令"而

跳过。

"Step Out"(Shift+F11):在第一种意义下,是执行到栈里最近的一个返回地址处(即执

行完当前函数);在第二种意义下,是执行完当前过程,在最近一次call指令的下一条指

令处中断。

"Run to Cursor":运行到当前光标所在处中断。

五、终止调试

要终止一次调试回到编辑状态,"Build->Stop Debugging"(Shift+F5);

要在终止后立刻重新开始(这往往是由于没控制好执行而越过了目标),"Build->Restart

"(Ctrl+Shift+F5)。

六、断点设置

最简单的断点莫过于位置断点:把光标移动到想要中断的源代码行处,按下F9,即可设

置或取消该处的断点。

所有的断点都可以在"Edit->Breakpoints"(Alt+F9)的对话框里管理。在这里可以设定更

多复杂的断点。

1、条件断点

选择要修改的断点,按下Condition按钮,即可设定在该断点处中断的条件。

注意此处的Expression填写的是C语言的表达式;可以有变量参与,但是如果含有不存在

的变量,调试器会禁用该断点。

2、数据断点

这是一类很强的断点。它没有固定的位置,只要求一个表达式;中断仅发生在表达式成

立(即结果为真)时。比如,填上表达式"argc==3",那么不论在何处,只要argc被赋值为

3,就会引起中断。

这个功能是CPU提供的,并不是靠调试器一边单步执行代码,一边检查表达式:)

3、窗口消息断点

因为我很少写GUI程序,所以对它了解甚少。想要使用它,请自己多试试吧:)

本篇着重叙述能够加快调试过程的一些技巧。

一、快速调试

在Win2000以上的系统上,对任何一个调试中(但还未被中断的)程序按下F12,可以立刻

中断此程序以进行调试。好比让调试器Break一样。

二、程序的上下文

在程序已经被中断后,通过"Debug->Threads"可以在多线程调试下,选择所关注的线程

或管理线程状态。"Debug->Modules",可以查看程序地址空间里模块的分布情况。

另外留意Variables调试窗口的上方,也可用于快速切换函数调用的上下文。

三、异常管理

熟悉调试器对异常的处理流程,对调试有异常处理的代码很有好处。以前未弄清楚这些

事情的时候,我都是采用注释的方法,很不方便。

通过"Debug->Exceptions"可以设置调试器对异常的动作。

在调试器层面上,对异常的处理分为两类:一类是"Stop always",一类是"Stop if

not handled"。下面解释这两者的区别。

异常都会以统一的格式报告给调试器。当某异常第一次触发时:调试器可以立刻停止程

序的执行,这就是"Stop always";调试器也可以先让操作系统展开异常处理(如__try/_

_except等等),如果程序没有代码来处理该异常,那么操作系统再一次向调试器报告异

常,此时调试器再中断,这就是"Stop if not handled"。

(由于"Stop always"选项)第一次中断,MSVC会提示"First-Chance";

程序没有处理该异常的代码而导致第二次中断,MSVC会提示"Unhandled"。

举个例子,有以下代码:

int main(void)

{

int a=0;

__try

{

a/=a;

}

__except(1)

{

printf("divided by zero!");

}

return 0;

}

两种对被零除异常的处理方法会影响到调试器是否在"a/=a"处中断。

如果被设置为"Stop always",异常在触发,再次执行后,调试器会向用户询问是否要将

异常传递给程序。也就是是否需要展开程序的异常处理代码来自行处理异常。选"否"的

话,调试器不会使用程序的异常处理代码,而继续执行程序。

这里的"继续执行"有两种意思,一种是从断点处重新尝试执行,另一种是从断点的下一

条指令处执行。这依赖于异常处理指令(__except里的参数)和异常的类型。在此不再赘

述。

整个处理流程可用流程图表示:

|

| 异常!

|

-------------- N ------------------- Y ----------

| Stop always? | ---+--->| 程序有异常处理吗?| --->| 处理异常 |

-------------- | ------------------- ----------

Y | | N | |

-------------- | -------------- |

| 中断程序执行 | | | 中断程序执行 | |

-------------- | -------------- |

| ^ | |

-------------- ^ -------------- |

| 用户操作 | | | 用户操作 | |

-------------- | -------------- |

| | | |

------------------- | | |

| 向程序传递异常吗?|- Y | |

------------------- | |

N | | |

-------------- | |

| 继续执行 |<-----------------+----------------------

--------------

|

|

四、Watch窗口的特殊指令

Watch窗口虽然很灵活地支持几乎所有C表达式,但有时仍让人觉得很不方便。想要高效

率地使用Watch窗口,就必须记住一些特殊用法。

1、监视系统调用

在Windows上编程的朋友一定对GetLastError系统过程很熟悉;GetLastError用一个全局

量记录系统在最近一次系统调用后的成功标记或失败原因。

在Watch窗口里输入"@err",就会显示此时调用GetLastError的返回值(32位无符号整数)

例如:假设调用CreateFile试图打开一个不存在的文件;那么在CreateFile返回后,"@e

rr"会显示"2"。

2、错误代码翻译

利用后缀",hr",可以实现"Error Lookup"小工具的功能。它把一个整数翻译成一条解释

原因的文字。例如:"3,hr",则会显示"0x00000003 系统找不到指定的路径"。

hr后缀的最佳用途是配合@err。例如上述失败的文件打开操作,"@err,hr"将显示"0x000

00002 系统找不到指定的文件"。对于定位错误原因非常方便。

3、数值格式

Watch有默认的显示进制,即十进制与十六进制。可以通过Watch窗口上的快捷菜单来选

择。在需要明确指明显示格式的情况下,可以添加后缀来指示。请注意,这里是区分大

小写的。

c:按照ASCII字符显示;

x或X:按照无符号十六进制显示;

d:按照有符号十进制显示;

u:按照无符号十进制显示;

o:按照无符号八进制显示;

另外有两种修饰符(必须配合上面的后缀):

l:按照32位显示;

h:按照16位显示。

例:

65,c:显示'A';

0x12345678,d:显示305419396;

0x12345678,hx:显示0x5678。

Watch也支持浮点数的显示:

不指定后缀:双精度;

f:按照有符号单精度显示;

e:按照科学计数法显示;

g:以上两者选出较短的一种显示;

例:

1./3:显示0.33333333333333;

1./3,f:显示0x333333

4、字符串格式

值得留意的是显示Unicode字符串。有以下后缀:

s:按照ANSI字符串显示;

su:按照Unicode字符串显示。

5、内存映像

其实这里就是完成Memory窗口的功能。不过它没有Memory窗口的缺点:比如每一次只能

从一个地址显示(因为Memory窗口就一个),不能格式化显示等等。

Watch窗口里通过添加后缀来指定显示格式。

ma:用ASCII字符连续显示64个字节;

mb或m:用字节格式显示16个字节,然后再用ASCII字符再次连续显示这16个字节;

mw:连续显示8个字(16位);

md:连续显示4个双字(32位);

mq:连续显示2个四倍字长的字(64位);

mu:连续显示8个字,然后再用Unicode字符再次连续显示这8个字;

例如:

0x400000,mw:显示0x00400000 5a4d 0090 0003 0000 0004 0000 ffff 0000

0x400000,mq:显示0x00400000 0000000300905a4d 0000ffff00000004

6、数组显示

这是很重要的功能。由于C语言在函数声明中不区分指针和数组----例如"int func(int

a[100])"和"int func(int* a)"是没有任何区别的----所以即使用一个数组去调用一个

函数,那么在函数执行过程中,Watch窗口也只把它当成一个指针,因而只能显示第一个

元素。这是编译器从根本上造成的。

Watch窗口支持在数组名后加上一个数来指示这是一个数组和它的宽度(元素的个数)有

多少。例如:"a,100":则把该项展开,就可以看到a[0]到a[99]。

7、其它

wm:宏显示Windows消息码;

比如:0x0010,wm:显示WM_CLOSE;在调试窗口过程中极为有用;

伪定时器@CLK:

@CLK用于显示一个32位无符号整数;这个整数与时间有关系,可以当作简单的计时器。

另外,掌握常用命令的快捷键对加快调试也很重要(这不废话么~~~)。

请参考相关文档:)

前两篇介绍了MSVC调试器的基本用法。本篇不再关注调试器,而把注意力放在代码的编

写上,阐述一些能够主动配合调试的方法。

一、OutputDebugString & afxDump

OutputDebugString是Windows系统提供的函数。它接受一个字符串,并把字符串---以一

种特别的调试事件---传递给调试器。

从程序角度讲,如果程序没有被调试,这个函数的调用没有任何作用。但当调试器存在

时,调试器就能得到这个字符串。比如程序被MSVC调试,并调用了OutputDebugString,

那么MSVC的Output窗口的Debug栏就会显示出该字符串。

它有以下优点:

1、适合用于程序流程的分析,不需要一次又一次地中断程序以查看变量;

2、适合调试与界面有关的代码;因为此时中断程序可能会导致程序的界面受到影响(因

为调试器本身也有GUI,所以会发生遮挡等等),而没法达到预期的调试。OutputDebugSt

ring没这个缺点;

3、适合调试非控制台程序;这类程序没有标准输出设备,所以printf函数不能使用,只

能用OutputDebugString;

MFC有一个afxDump全局量来包装OutputDebugString。

比如:"afxDump<<56<<str",用法更简单。

二、宏_DEBUG

_DEBUG宏用于指示此次编译是Debug模式,还是Release模式。早在第二篇开头就已说明

:编译器是没有Debug和Release的区分的。

为了编写一段能够在两种模式下进行不同处理的代码,MSVC帮助我们定义了这个宏(当然

也可以通过编译器开关-D自己定义另外的宏)。

比如有以下代码:

#ifdef _DEBUG

printf("a=%d/n",a);

#endif

可以在Debug模式下,会执行中间一行的代码;而在Release模式下,编译器根本见不到

这一句。故在代码里用这种形式嵌入辅助调试的代码是不会增加发布时的成本的。

三、宏__FILE__,__LINE__

这两个是编译器维护的宏。__FILE__指示当前编译的文件的文件名,__LINE__指示正在

编译的代码位于文件的多少行。

这两个宏非常重要。它们是断言(assert)的实现基础(因为assert会在断言失败时报告出

错的文件名及行数),也是程序员能够使用的很好的调试手段。

考虑以下代码:

void error(char* s) {

printf("error - %s/n",s);

exit(-1);

}

这种代码被频繁使用,能够在程序的不同地点作相同的错误处理。

但是一旦出错了,要如何来判断到底是在那里调用了error呢?这就又不得不在error里

设立断点,重新执行,利用调试器的栈遍历功能来确认。

事实上利用上面两个宏,就可以避免启用调试器。修改代码为:

#define error(s) /

{printf("error - %s @ file: %s line %d/n",s,__FILE__,__LINE__); /

exit(-1);}

立刻就能从出错信息中看到出错的文件名和行数,根本用不着调试器。这样不是更方便

吗?

四、预处理指令#line

#line与__LINE__和__FILE__相反。后者是获取当前行数或文件名,而前者是设定当前行

数或文件名。

如果把后两者看成全局变量,那么#line 100 "xyz.l"就相当于__LINE__=100和__FILE__

="xyz.l"。

#line在某些场合很有用处,例如在代码生成器里。以语法分析器的产生器flex为例,fl

ex扫描一个有特定语法的语法文件,生成一个C文件作为语法分析器。

用过flex的都知道,flex里有时需要嵌入代码。这些代码会被flex保留到生成的C文件里

这样问题就出来了:编译器只能编译生成的C文件,所以当嵌入代码出错时,调试器只能

在C文件里定位。通常这个C文件的体积是很壮观的,很难下手分析。

所以希望调试器能主动定位在初始的语法文件里。此时,#line是最好的选择。

例如有以下代码:

int main()

{

int a=0;

#line 1

a/=a;

return 0;

}

编译并启动调试器,看看在a/=a出错时,调试器跑到哪里去了:)

五、断言(assert)

assert函数是专门用于调试得,在assert.h里定义。

assert接受一个数值,如果它为真则没有动作,不然弹出对话框让用户选择“忽略”、

“调试”还是“中止”。

assert用于先决条件的检测,是避免错误的最有效的手法。assert在Release编译下被定

义为(void)0,显然assert不会增加发布时的程序成本。

例如:

assert(a==b);

果在Debug模式下编译,当执行到该句时如果a!=b,则会立刻引发中断;

果在Release模式下编译,没有这一句。

assert可以模拟条件断点,比如:

assert(!(a==b))当a==b时中断(写成宏就更方便了)。

这就避免使用调试器的条件断点了(不然在Breakpoints对话框设置太麻烦)。

再例如:

assert(0);

这是一个必定失败的断点,在分析代码流程时很常用。

assert的语义很简单。用法虽然容易,里面却也不乏各种技巧。可谓博大精深的一个函

数。

我见过的最夸张的用法是:

assert(!"never come here!");

看明白了吗?这也是一个必定失败的断点。此句出自Numega公司的重要人物John

Robbins之手。可见此人懒得出奇,连注释也放到assert里了……

六、其它

1、RTTI

RTTI即运行时类型识别,可用于检测一个类对象的类型。

比如某个函数接受一个类型为A(这是一个类类型)的参数。显然,只要从A继承下来的类

类型都可以传递。但如果期望此函数不接受某种派生类型,用RTTI配合assert比较方便

2、ASSERT_KINDOF、ASSERT_VALID等等

基于MFC的(类型)断言。还有各种变形,功能很强,但只能用于MFC程序。

以上两者请参考相关文档。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。