Turbo Pascal programs start by calibrating a delay loop (so that the Delay
function knows how much to spin to achieve a certain delay). The calibration counts the number of times a certain loop is run for 55ms (as measurable using the PC’s timer interrupt with its default setting), then divides the number of loops by 55 so that Delay
can then busy-wait in millisecond increments. On fast CPUs, 200MHz and up (on Intel CPUs), the loop runs too many times, and the division overflows. The CPU throws a “divide overflow” error, which the Pascal runtime reports as a division by zero error.
There are quite a few sites which explain this and provide patches; for example, J R Stockton’s page on the topic, which says
The Borland Crt unit is included in the TURBO.TPL & TPP.TPL libraries; its initialisation routine will be linked if Crt is cited in a uses clause. The problem lies in the initialisation of Crt.Delay, but will appear if the Crt unit is cited regardless of whether Delay or any other Crt routine is called.
During Crt unit initialisation, a loop executed for 55 ms increments a counter. Up to and in TP6, this was a 16-bit counter, and would happily overflow on a PC above about 20 MHz, leading to subsequent incorrect delays.
The counter in TP7 & BP7 is now 32-bit, and should not itself overflow until processor speeds reach the 100 GHz region. But the count is divided by 55, and if the result cannot fit into a 16-bit word, the CPU raises a "divide overflow" error. This is reported by Borland as a "divide by zero" error, Runtime Error 200, since the only way that a user's Pascal code can cause a divide overflow is by dividing by zero.
(As pointed out by Peter Cordes, the last sentence is incorrect; it’s easy to cause a divide overflow using integer division in Pascal. Borland’s runtime does however report that as a “divide by zero” error.)
Arguably the best fix is to fix the Crt unit, and relink Pascal program. There are various approaches; for example, increasing the space allocated for the delay counter in CRT.ASM
:
DelayCnt DD ?
(instead of DW
), then altering the calibration routine to use both words instead of a single word.
Patching a fix into existing executables isn’t all that obvious since the fixed calibration routines take more space than the original, but that’s exactly what Andreas Bauer’s patch (available on SAC, Garbo, or manassehkatz’s site) does: he shortened earlier initialisation code to make room for his fixed calibration routine, as detailed in the README
file in his archive. Andreas’ patch doesn’t increase DelayCnt
’s size, it only ensures that the calibration routine doesn’t overflow; as a result, on fast CPUs the Delay
routine might not wait as long as intended.
Another approach is used by c’t’s patch: it relies on shortening another function in Crt (Break
) to free up space for an improved version of Delay
, and adjusting the divisor in the calibration routine so that the division no longer overflows. The calibration routine’s result isn’t used in this scenario.
There are also a number of TSRs which will handle the problem at runtime, without patching; one significant difference here is that most (if not all) such TSRs won’t work with protected-mode Turbo Pascal programs. Here also there are number of different approaches. PROT200
relies on handling the divide-by-zero error in the TSR instead of letting Borland’s code handle it. TP7P5FIX
hooks the interrupt setup function in DOS and intercepts the initialisation code when it tries to setup its divide-by-zero handler, instead patching the initialisation code to return the highest possible value (0xFFFF). R200FIX
patches dummy OUT
instructions into the delay loop, which allows it to provide correct delays. (Thanks to Michael Karcher for the investigations.)
Delay
by a new, multitasking-friendly (for long delays) implementation. Is RCSE interested in that tool? Should I go polish it up?