Borland公司推出的turbo/borland系列编程环境,在dos时代,以其快速的编译速度,优良的代码效率,和功能强大的集成环境,以及丰富的开发库曾一度占领了当时的开发工具的是市场。过去的程序员很少有人没有用过大名鼎鼎的turboC/borlandC/C++和borland Pascal的。即使是在现在windows大行其道的时候,仍然还是有一批忠实的用户。turbo/borland系列仍然是编写dos下的程序的最好的编程环境。尤其是对于大多数目前的高校而言,讲授程序设计语言和数据结构时,仍然大部分是用Borland Pascal的。
Borland Pascal 7.0是Borland公司于1992年推出的著名的Turbo Pascal 系列的新版本。它比Borland C/C++的优势在于它可以将pascal程序编译成为保护模式下的程序。这就意味着开发者可以使用所有的内存,它不用像BorlandC++那样要考虑640k内存的问题。但是这样优秀的开发环境,其最为常用的CRT单元,存在着一个致命的错误,使得用了crt库的程序,都将无法运行。
最初发现这个错误是在编译Borland Pascal所带的一个例子程序crtdemo.pas,发现一运行就会出现divided by zero.(除零错误)的错误信息。单步跟踪它,发现程序其实尚未运行,即单步跟踪条尚未到begin语句,就出现了上述错误。看来并不是crtdemo程序的原因。后来又发现这个错误其实也不是每次都发生,如当win98运行其他很多程序时(我是在win98下运行bp.exe的),有时它也不会出现错误的。但是大多数情况下,它出现错误的几率大得多。
于是采用Turbo Debugger跟踪该程序。发现在执行到cs::0078时有如下语句:
cs:0078: mov BL,ES:[DI]
cs:007b: cmp BL,ES:[DI]
cs:007e: je 007B
这几条语句的作用是把ES:[DI]的值放入BL中。但是又立即比较BL和ES:[DI]的值是否相等,相等则继续比较。但是这个值是刚放进去的,当然会相等,而且只要不改变这个值,它都会相等,这不是死循环了吗?于是查当时的ES:DI值,是40H:6CH,再查参考资料,原来40H:6CH的值是BIOS时钟计数器的双字单元。这个时钟单元每1/18.2秒(即55毫秒)改变一次。然后在气后面几含有出现了这样的语句:
cs:008e mov cx,0037(即十进制的55)
cs:0091 div cx
看来这一段程序是求出每毫秒的计数值的。于是分别在0091之前和之后下了断点,正是确实是在0091处出现除零错误的。
既然找到了问题所在,当然就得想办法解决。于是找到Borland Pascal Runtime Library(Borland Pascal运行时间库源代码,在BorlandPascal的第十一张安装盘),找到CRT.ASM,察看其源程序。这段程序是:
MOV ES,Seg0040
MOV DI,OFFSET Timer
MOV BL,ES:[DI]
@@2: CMP BL,ES:[DI]
JE @@2
MOV BL,ES:[DI]
MOV AX,-28
CWD
CALL DelayLoop
NOT AX
NOT DX
MOV CX,55
DIV CX
MOV DelayCnt,AX
(delayLoop的程序段是:
DelayLoop:
@@1: SUB AX,1
SBB DX,0
JC @@2
CMP BL,ES:[DI]
JE @@1
@@2: RET)
粗看起来这段程序一点问题也没有。最前面的几行是让程序在下一个55毫秒来的一刹那继续。然后调用DelayLoop计数,计数的结果再除以55,就可以得到每毫秒的循环所需的计数值,放入DelayCnt中。为后来的Delay过程所用。那么问题处在哪里呢?难道是计数值超过了65536*55,以致于溢出?
为了证实我的猜想,我特地编写了一小段汇编程序:
.model small
.code
start proc near
mov ax,0040h
mov es,ax
mov di,6ch
mov bl,es:[di]
loop1:cmp bl,es:[di]
je @@1
mov ax,0
cwd
mov bl,es:[di]
loop2:
add ax,1
adc dx,0
cmp bl,es:[di]
je @@2
int 3
mov cx,55
div cx
mov ax,4c00h
int 21h
start endp
end start
用td跟踪。运行的结果果然不出所料,每次的计数值大多数都是使dx的值超过了37h。这样的话当然会使除以cx的结果产生除零错误。看来这个错误的产生,是由于BP的开发人员,没有考虑到机器的速度会变得这么快,因此,只使用了一个字来存储计数值。所以造成了这样的一个溢出错误。这也真可以解释为什么在win98下运行很多程序时,系统负荷比较重,速度下降,这时计数值反而不会溢出,因而不会出现除零错误的原因。
最初我的解决办法是将div cx改为nop,这样当然能够避免除零错误。但是这样做的后果是使delay过程的延时功能失效,因为delaycnt的值和实际值是不同的。于是我就干脆修改crt.asm,修正这个错误。具体的思想是:将计数值delaycnt改为双字,使其能够表示大于65536的数值。并对原程序中使用的字寄存器改为32位寄存器。
具体的做法是:
编辑crt.asm:
①在第45行delayCnt dw ?后加上一行dw ?
②将上文的程序段和delayloop子程序改为:
程序段:
MOV ES,Seg0040
MOV DI,OFFSET Timer
MOV BL,ES:[DI]
@@2: CMP BL,ES:[DI]
JE @@2
MOV BL,ES:[DI]
DB 66H,0B8h,0D8h,0FFh,0FFh,0FFh ;Mov eax,-28
DB 66H,99H ;CDQ
CALL DelayLoop
DB 66H
NOT AX
DB 66H
NOT DX
MOV CX,55
DB 66H
DIV CX ;div ecx
DB 66H
MOV DelayCnt,AX ;MOV DelayCnt,EAX
delayLoop的程序段修改后是:
DelayLoop:
@@1:
DB 66H,2DH,01H,00H,00H,00H ;SUB EAX,1
DB 66H,83H,0DAH,00H ; SBB EDX,0
JC @@2
CMP BL,ES:[DI]
JE @@1
@@2: RET
③重新编译CRT库:
设置好BorlandPascal的路径:set path=d:\bp\bin (因为编译时要调用bpc和tasm)
使用make重新编译全部单元
MAKE
然后再将新生成的CRT单元拷贝到BP\BIN目录下
COPY D:\BP\RTL\BIN\TPU\CRT.TPU D:\BP\BIN
④将新的CRT库加入到TURBO.TPL中去。
在BP\BIN目录下:
TPUMOVER TURBO.TPL -CRT(将旧的CRT单元移出)
TPUMOVER TURBO.TPL +CRT.TPU(加入新的CRT单元)
对于那些没有RTL运行时间库的用户而言呢。因为无法重新编译,修改量太大。所以干脆就只是把DIV CX去掉即可。在这样的情况下,只要不使用delay延时,不会出什么大的问题。修改的方法如下:
①(将crt.tpu从turbo.tpl中取出来)在bp\bin目录下:
tpumover turbo.tpl *crt.tpu
②修改crt.tpu
debug crt.tpu
-s 0 FFFF F7 F1 A3
这时返回的地址应该为DS:0931则
-E 0931 90 90 (将其改为NOP)
-W
③将修改后的CRT.TPU加入到TURBO.TPL中
tpumover turbo.tpl +crt.tpu
Borland Pascal的这个问题是比较严重的。比如以前用Borland Pascal写的使用了CRT单元的可执行程序,现在在很多高速的机器上就无法执行了。如果有源程序的话,可以在按照本文的方法修改了CRT单元后,重新在编译一遍。否则的话,就只有用debug修改可执行文件,仿照第二种办法去直接修改,将div cx改为nop了。
另外,因为这个错误是因为机器速度过快造成的。所以在一些速度较慢的机器上,可能不会出现除零错误的现象。但是还是建议对CRT的这个bug进行改正,否则很可能出现以后你用BP编译的程序无法在更高配置的机器上运行的现象。本文的程序在Celeron366/32M内存/Pwin98的MS-DOS方式通过。
|