这一次我们来练习抓虫虫!
程式设计师绝对都曾经有过与「臭虫」奋战的经验,你是否也曾经为了一个程式的臭虫忙到深夜後来发现问题出在一个实在很蠢的地方?优异的开发环境如Delphi者当然不会忘记除错器(Debugger)这样重要的工具,然而 Delphi安装完成後,循开始功能列一路找到Borland Delphi 3 的程式捷径群组时却没有发现什麽除错器!?
嗯!别耽心,Delphi的除错器不仅没有缺席,而且设计得几乎不输给任何独立的除错程式。至於我们为什麽却找不到它,那是因为它直接属於整合开发环境的一部分;换句话说,我们在完全不离开整合开发环境下直接与IDE其他的相关工具共同合作侦错程式。
首先,将Delphi的除错开关全部打开吧!请您跟我们这样做:
- Delphi主选单【File / New Application】开始一个新的专案。
- 【Tool / Environment Options 】在 Preferences这页(如图一)将Integrated debugging与Break on Exception这两个选项都选上。Break on Exception则指的是当程式发生例外(Exception)时,是否要暂停并以醒目的色彩标定出例外(错误)发生的那列程式。根据经验,Integrated debugging若是没有选的话,Break on Exception不会有作用;而且,如果Integrated debugging不选的话,等一下我们也没办法进行程式追踪。
- 【Project / Options┅】,切换到在Compiler这页,将左上角的Optimization程式最佳化的功能关掉。(如图二)
- 一样是刚才那个地方,将右半边的Runtime errors、Debugging与Message这三大项的选项全部打勾(亦如图二)。
经过2-4的设定之後,Delphi的除错功能算是全开了。
关於第四个步骤所设定的各选项其用意是这样的:Messages中的Show hints与Show warnings是请Delphi指出我们程式中某些浪费的或者是写法可能有问题的地方。例如什麽呢?
procedure TForm1.Button1Click(Sender: TObject);
var
AButton: TButton;
i, j: integer;
begin
for i := 3 to 1 do
Beep;
AButton.Caption := 'Oh! No';
end;Delphi会告诉我们那个For回圈是零次根本不执行,还有J变数定义了却从来没用到,最後比较严重的是AButton还没有建构我们就拿来使用了。Show hints与Show warnings提醒与警告我们的就是这些,简单的说,它提醒我们程式的虽然语法正确但是它认为不太对劲的地方。
至於「Debug Information」是专案中各单元有关除错的主要设定,将这个选项打勾,将使得Delphi的程式单元(.PAS)在编译时加入额外的除错资讯到编译後产生的.DCU(Delphi Compiled Unit)中,这麽做的影响主要有以下两点:
- 能够使用 Step Over 或 Trace Into 等功能逐步追踪程式的执行流程,有关Step Over 与 Trace Into的使用稍候将会说明。
- 当程式遭遇到执行时期错误(Runtime Error)时,我们得以经由如图三显示的错误位址,以IDE主选单 【Search / Find Error】找出发生错误的程式列。
例如以下的程式,很明显的对记忆体进行不当的存取,这样的程式当然会引发如图三这样的错误:
procedure TForm1.Button1Click(Sender: TObject);
var
p: ^Integer;
begin
p := nil;
p^ := 100;
end;哇!程式出错了,不要紧张,东西还没有出货给使用者前赶紧改掉。现在,请将图三的错误位址记下来,然後以Delphi重新开启这个专案,编译之後以IDE主选单 【Search / Find Error】可以找出程式出错在哪一列。值得注意的是,若你是使用Delphi 3.0x的话,直接拿这个数字输入到Find Error对话盒时,也许会发现Delphi完全无法找出错误的程式!?之所以会发生这样的情况,有以下三种可能:
- 错误发生在VCL中,因为Delphi的VCL是在关掉Debug information的情况下编译的,以致於没有线索找到原始程式。不过,一般来说绝少发生这样情况(至少我自己还没有遇到过),往往到最後发现还是我们自己程式的问题。
- 编到EXE档的程式被编译器的最佳化功能动过手脚了,与我们的程式已经不太相同了,因此没办法按错误位址找到出错的那一列程式。这也就说明了为什麽在除错时要如图二将Optimization程式最佳化的功能暂时关掉的原因。
- 在Delphi 3要先减去Image base的值。请查一下【Project / Options / Linker / Image base】的设定值($400000是预设值),错误位址减掉这个值就对了(以本例来说就是$25894)。
当然啦!这是测试小组给回来的错误报告,如果在此之前是自己在IDE跑这个问题程式,而且稍早提到的IDE除错开关Break on Exception也按照设定让它有作用的话,程式就会暂停在发生错误的地方并以醒目的颜色标示出错误的程式列。(如图四)其他的设定像是Local Symbols这个选项的用意在於是否将区域变数的除错资讯在编译时一并加入.DCU中,这麽做的结果是未来我们可以用【View/Watches】检视这些区域变数的值,甚至也可以使用【Run / Evaluate/Modify】临时改变区域变数的值。Symbol Info 的用意在於专案在编译之後以【View / Browser】观看专案中各物件的内容与彼此的继承关系图时,能/不能特别看出类别定义在哪一个单元以及它由哪些单元叁考使用等资讯。
Overflow checking 是用来检查整数数值运算是否溢位,而Range checking是检查字串与阵列的存取是否超出合理边界范围、子范围或列举型态是否在定义的范围之内,数值的指定叙述是否满溢等问题。
也就是说,编译器会额外产生一些程式码帮我们检查整数数值运算是否满溢!举例来说,WORD型态容纳的数值是从0到65535,因此,如果再将已经是65535的WORD型态变数再递增一的话,就会举发出EIntOverflow的例外。请看以下的程式:
// 第一个版本
#0001 {$Q-}
#0002 procedure TForm1.Button1Click(Sender: TObject);
#0003 var
#0004 i: WORD;
#0005 begin
#0006 i := HIGH(WORD);
#0007 ShowMessage(IntToSTr(i)); // => 65535
#0008 Inc(i);
#0009 ShowMessage(IntToSTr(i)); // => 0 !!!!
#0010 end;以这个程式来说,I的值已经到顶了,但是程式仍然亳无警觉的执行完毕,这实在是很危险的情况,是吗?现在,您可以将0001改成{$Q+}试试看,在0008这列就会举发EIntOverflow的例外了。
程式中的{$Q}是用来告诉Delphi如何编译程式的所谓「编译指示」,对应到图二的Overflow checking,事实上,图二中的每一个选项都有一个对应用的「编译指示」。也就是说,我们除了可以在图二的画面中设定各个开关让整个专案各个程式都比照办理,也可以在程式中插入这些看起来像注解的东西来局部的控制编译器的行为。
如果没有问题,请再看以下的程式:
// 第二个版本
#0001 {$Q+}
#0002 procedure TForm1.Button1Click(Sender: TObject);
#0003 var
#0004 i: WORD;
#0005 begin
#0006 i := HIGH(WORD);
#0007 ShowMessage(IntToSTr(i)); // => 65535
#0008 i := i + 1;
#0009 ShowMessage(IntToSTr(i)); // => 0 !!!!
#0010 end;咦!已经是{$Q+}了,怎麽程式执行起来还是浑然不知啊?唉!因为{$Q}说,这不是我管的啦。{$Q}与{$R}(对应到图二的Range checking)就像是主管类似事务的两个单位,第一次去洽公的人总容易碰钉子。好啦好啦!我不要拐着弯骂人,这个程式请将0001这列改成{$R+},就会在0008举发例外了,但不是EIntOverflow,而是ERangeError。
按照Borland的解释,在{$Q+}时,会对Inc等运算函数进行检查,其他的运算若在计算过程中发生的溢位,算是EIntOverflow,归{$Q}管;发生在指定叙述时,是ERangeError,归{$R}管。什麽叫计算过程中发生溢位呢?嗯!那要看CPU暂存器有多大,第二个版本在进行I + 1时,Pentium的暂存器要容纳这样的数字是亳无困难的,真正有问题是发生在这个计算结果要放进I变数时。经过这样的解释之後,相信你就了解为什麽以下的第三个版本,举发的会是EIntOverflow了吧!
// 第三个版本
#0001 {$R+,Q+}
#0002 procedure TForm1.Button1Click(Sender: TObject);
#0003 var
#0004 i: integer;
#0005 begin
#0006 i := HIGH(integer);
#0007 ShowMessage(IntToSTr(i)); // => 2147483647
#0008 i := i + 1;
#0009 ShowMessage(IntToSTr(i));
#0010 end;这是整数的运算,那浮点运算呢?这倒不用担心,即使是{$Q-}{$R-},溢位一定会举发EOverflow例外:
{$Q-,R-}
procedure TForm1.Button1Click(Sender: TObject);
var
d: double;
i: integer;
begin
d := 2;
for i := 1 to 100 do
d := d * d;
ShowMessage(FloatToStr(d));
end;我想你一定仍然很耽心 我怎麽知道什麽该谁管啊!所以,要让这两个单位不互踢皮球的最好方法,就是一旦有事,叫它们两个单位都负起责任。我们的建议是:不要在这种地方钻牛角尖,乾脆将{$Q}{$R}一起打开或者一起关掉。:p
好了,现在您已了解Delphi的除错开关了,接下来的问题是:我们应该打开这些开关吗?答案是:看情况。如果是程式研发期间特别是进行除错时,当然,完全按照刚才1-4的步骤。如果程式要交货的时候呢?那Optimization最佳化功能应该恢复,Range checking、Overflow checking可以考虑关掉,因为这两项会额外的产生一些检查码,因此使得执行档变大些也变慢些。不过,我们的意见是先不忙着这麽做,等系统上线一段时间以後再说吧,往往,使用者总还是有办法让你的程式出一点小状况的。
1-4设定了不少选项,除了刚才提到的这三项呢?我们建议您保留它们打勾勾的情况,因为这些只会对.PAS编译出来的中间产物.DCU有变大的作用,最终的产品.EXE并不会有速度与档案大小方面的影响。
经过上述的设定之後,Delphi已经会提醒你程式中的问题写法,真的出错时,程式也会暂停在出错的那一列程式。不过,真正棘手的是:程式的执行结果就是不对,不对就是不对。怎麽办呢?
首先,要让程式停下来,以Delphi的术语来说是设定一个中断点(Break point)。怎麽做呢?在你编译过程式後,程式编辑器的左边是不是出现一些蓝色的小圆点?移动滑鼠指标点它一下就行,或者你也可以使用主选单【Run / Add Point】,然後点一下「New」按钮。这样,游标所在的那一列程式也会以明显的颜色(预设是红底白字)框起来,等一下程式执行到这列时就会暂停下来了。
嗯!设定中断点也是有学问的,我想应该没有人是只要程式一有错就从闷着头从第一列查到最後一列吧!通常会在怀疑有问题的附近才设中断点,先设定中断点让程式暂停然後逐步追踪逼近才是一般的作法。策略上是采用各个击破的方式,将程式分成几个模组或段落,分别进行检视验证,通过了再放行然後继续下一个模组,相信一定会找出Bug藏在哪 !
为了示范Delphi的除错器,我们特别设计以下的程式备用 没有什麽实用性啦!唯一的功能是我们可以用它来说明除错器的使用。如果你已经按本文一开头的那四个步骤设定完成後,接下来就是放个TButton按钮到Form 头然後点它二下,键入以下的程式:(行号只为说明方便,并不是程式的一部分)
#0001 ┅
#0002 procedure Test;
#0003 var
#0004 i, x: integer;
#0005 begin
#0006 for i := 1 to 100 do
#0007 begin
#0008 x := i;
#0009 if (x = 90) then Exit;
#0010 end;
#0011 Beep;
#0012 end;
#0013
#0014 procedure TForm1.Button1Click
#0015 (Sender: TObject);
#0016 begin
#0017 Test;
#0018 end;
#0019 ┅编译之後,按刚才的说明直接以滑鼠点选程式编辑器左方蓝色的小圆点。图五即是在「Test;」这列以滑鼠点选设定了程式中断点之後的情况。取消中断点的方法则还是以滑鼠点一下程式编辑区的左边最为方便。
好了,现在程式执行时会在点了Button1这个按钮後,暂停在呼叫Test程序这列程式上头,接下来我们有两个选择:一则是【Run / Trace Into】(或者按下F7功能键);一则是【Run / Step Over】(或者按下F8功能键)。
假如Test已知是可靠的或者没有深入追踪的必要,可以使用F8跳过去(Step Over),程式会全速执行Test的呼叫然後很快就返回,接着继续停在Test;的下一列。
反之,我们也可以继续深入追踪进Test这个程序,一步一步看看它是怎麽执行的。按下F7(Trace Into)之後,程式会停进Test这个程序 。进入之後,若没有再遇到函式呼叫,按F7或F8都一样是一次一个叙述,没有深入追踪或是否跳过的分别。
追进Test没有多久之後就是一个回圈,於是就开始一直循环在这个回圈中,除非这个回圈真有追踪的必要,否则一直按F7/F8键还真是磨练耐心的好机会。如果你觉得这个回圈实在没有侦查的必要却又一直在那边一步一步的绕很讨厌的话,那麽,使用滑鼠点一下「#0011 Beep;」这列,然後以【Run / Run to Cursor】(或者按下F4功能键),可以让程式直接执行到游标所在位置。
原来如此,我们可以使用F7(Trace Into)深入追踪某个函式,也可以当它是一般普通的叙述加以跳过(F8, Step Over),除了 F7/F8的单步追踪,F4(Run To Cursor)则可以快跑一段程式。深入或者略过,徐徐前进或者大步开跑就都如你所愿了。
最後,我再补充一个还蛮管用的中断点设定技巧。以刚才的例子来说,在#0009 if (x=90)┅这列先设一个中断点,然後【View / Breakpoints】,选择这个中断点接着经由滑鼠右键点到「Properties」(如图六),最後则在图七的画面中输入x=90,然後点「Modify按钮」。程式会在x=90时才暂停,这个技巧主要用来侦测某一列(段)程式在特定范围或条件时的反应(至少看看它会不会真的跑到),除了Trace Into与Trace Over之外,它也有助於你控制程式追踪的步调。
了解程式执行的步骤固然重要,往往变数的内容能够提供我们找出臭虫的线索,对於变数的内容,我们是以【View / Watches】利用Watch List 视窗进行了解的。当这个视窗出现时,点一下滑鼠右键并选择【Add Watch】,在Expression栏中输入变数的名称(例如本例的X)或者是一个运算式,当程式单步追踪到Test程序的回圈中时,我们经由【View / Watch】即可检视X变数的内容。
Delphi 3有更为方便的作法:如果你的【Tool / Environment Options┅ / Code Insight / Tooltip Expression Evaluation】选项是打勾的话,直接将滑鼠指标移到某一个变数上方,稍作停留後也会看到这个变数的内容。
修改变数的内容刚才会在x=90时跳出for回圈:
#0009 if (x = 90) then Exit;
当我们追踪到这列程式时,不管X是当时是多少,经由【Run / Evaluate/Modify】,Expression栏输入 x,点一下Evaluate按钮,然後在Vew value栏中输入90 (如图八),点了「Modify按钮」之後,x 这个变数的值就变成了90,程式也就随即因为 if 叙述的成立而跳离回圈。
原来如此,变数的内容不仅可以【View / Watches】观看,也可以动态利用【Run / Evaluate/Modify】进行修改。这样的技巧往往用来输入测试用的资料或者试探我们的程式对於不预期状况的反应时十分实用!
【Run / Call Stack】则是用来检视函式与函式呼叫的关系。以刚才的程式来说,当程式追踪到Test这个程序时,以Delphi IDE主选单【Run / Call Stack】看到的情形如图九所示,其中的【View Source】【Edit Source】则是按下滑鼠右键之後的快显功能表,利用这两个选项可以很方便的直接移到该函式的单元位置。
Delphi的除错功能过去有一个未公开的秘密(现在已经有不少人晓得了,Borland也已在其技术支援网页公开这项设定方法),使用Windows 95提供的Regedit.exe直接在如图十机码的位置,新增一个字串值ENABLECPU并设为「1」,下次启动Delphi时,IDE的主选单中将会多出一个【View / CPU Windows】,这个「Disassembly View」的视窗将会报告正在执行的程式的组言语言码以及各暂存器的状况。如果你需要比较低阶的除错功能,不妨在登录资料库 加入这项设定。
通常我们不会需要对於VCL进行除错的工作(这是Borland的工作,不是吗?兒),不过,如果你也对於VCL内部运作的细节很感兴趣,或者你真的怀疑错不在你而是VCL程式。那麽,不妨看看以下的这则示范:
- 将Delphi安装目录下的Source/VCL目录中的所有VCL原始码档案复制到另外一个专用的目录,例如:d:/TraceVCL。
- 启动Delphi,在Form上放一个TButton,对於Button1的OnClick则什麽也不写,begin..end;之间只单纯加个双斜线的注解即可。
- 将这个新的专案存到刚才放置VCL原始程式的同一个目录(d:/TraceVCL)。
- 【Run / Build All】重新编译这个专案。
- 在Button1Click这个事件处理程序的end处设一个中断点。
- 【Run / Run】执行这个专案并点一下Button1这个按钮。
- 以【View / Call Stack】观察各函式的呼叫关系(图十一),使用滑鼠右键从快显功能表中选择【View Source】则可移到某一函式的位置。
这是我们比较建议的方式,特别是对於VCL这麽大的Component Framework还不熟的情况下,一时间不一定知道该从何下手。在我们自己的程式设中断点,然後以此为基础追进VCL的相关程式是比较容易接近目标区的途径。
最後,我们想提醒您有关於编译环境设定的问题(也就是如图二的设定),因为它们可能在您没注意时制造出你意想不到的程式错误。
function IsGreateThen0(const x: integer): boolean;
var
s: integer;
begin
if x > 0 then
ShowMessage(IntToStr(x) + ' > 0 ')
else
ShowMessage(IntToStr(x) + ' <= 0 ');
Result := x > 0;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if IsGreateThen0(-20) and IsGreateThen0(20) then
ShowMessage('都大於零')
else
ShowMessage('其中有一个不大於零');
end;这个程式您认为会出现两个还是三个讯息盒呢?答案是:不一定。如果你在Button1Click的begin下一行插入一列{$B+},再执行时便是三个讯息盒了;改成{$B-}则是两个。Delphi出货时的预设值是{$B-}(请与图二的「Complete boolean eval」互相对照)。
事情是这样的,当一个逻辑运算式以各种and、or连接起来时,如果经由前面一部分的结果就已知整个逻辑运算式的结果时,Delphi可以让你选择程式不用将整个逻辑运算式都算完,这有点像是选择题看到第二个选项是答案就答题了。刚才的程式就是这样,第一个IsGreateThen0(-20)时是假值,後头是以and连接的,不管and之後的情况如何演变,整个if条件的结果一定是不成立的情况,因此,也就不用再管and的後面而直接给答案了。
对於逻辑运算以变数值进行判断时,这样的确提高一点执行速度,但是如果运算式中还有呼叫函式,而这个函式又因为逻辑值快速评估的关系完全没有执行到,程式会不会在後来的哪 出现错误就很难说了,是吗?因此,布林运算是不是要完整评估倒是值得注意的,否则程式不管坐着看站着看都不像是有错的样子,设了中断点也不见停留,最後怀疑Delphi编译器有Bug跑到网路上去闹就有点
再举一个例子:
type
MyRecord = record
ByteField: byte;
IntegerField: integer;
end;
┅
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(IntToStr(SizeOf(MyRecord)));
end;这题您认为显示的是「8」还是「5」?答案是都有可能。写程式可不能说都可以喔!如果我有一支程式认定记录长度是8写资料到档案,然後朋友的另一支程式认定记录长度是5到档案读资料出来,结果会是什麽?这个Bug我追过,很辛苦,因为从程式中完全看不出来问题出在哪 。
ShowMessage在{$A+}(对应图二的Aligned record fields,这是预设值)时显示的结果是「8」;倘若是{$A-},那所得的结果是「5」。按理说,Byte型态只要一个位元组就足够了,但是考虑到CPU的执行特性,对齐後的record会有比较好的执行速度。
经过这两个例子之後,相信您也发现编译指示(Compiler directive)的厉害了吧!关於这些,您可以叁考Delphi手册附录B的内容,或者到以下的网址下载中文的说明版本:http://www.chih.com ->主题公园。
在这一篇文章中,我们分享了Delphi整合除错工具的设定与使用,最後也谈到了一些编译指示对於程式行为的影响,有了这些,再配合Delphi的例外处理,相信对於您写作正确的程式一定大有帮助。过年了,敬祝大家新春平安如意!来年可以顺利的将程式中所有的不愉快一扫而尽。
图一 / Delphi整合除错环境的设定
图二 / Delphi关於专案的编译与除错设定
图三 / 程式出错时请留意其错误位址
图四 / 程式暂停在出错的那一列程式
图五 / 设定程式中断点
图六 / 要求设定中断点的条件
图七 / 设定中断的条件
图八 / 动态异动变数的内容
图九 / 以Call Stack观察函式的呼叫关系
图十 / CPU View的登录设定
图十一 / Button1.OnClick中断时的Call Stack