87

On faster MS-DOS systems, it wasn't entirely uncommon for applications built using Borland's Turbo Pascal to fail to start, and (before exiting back to the command prompt) to report a division by zero error immediately upon launch.

As I recall, this behavior could be observed even with simple "Hello world" style programs.

Patches were available which fixed the issue both for freshly built executables as well as already-built ones, but my question here is: What actually caused the error in the first place?

A great answer will also discuss what the fix was, and how it fit into the executable code without requiring a recompilation.

  • 1
    I found my sources for a TSR tool that automatically detects affected real-mode Turbo Pascal program on load and patches them accordingly (so no modification is of on-disk EXE files is needed). Alas, only in its most recent version that includes a failed experiment to add protected mode support, and completely in German. It completely replaces Delay by a new, multitasking-friendly (for long delays) implementation. Is RCSE interested in that tool? Should I go polish it up? – Michael Karcher Aug 21 at 12:40
  • @Michael there are a number of TSRs available, see here; if yours is different I guess it could be interesting! – Stephen Kitt Aug 21 at 12:51
  • 1
    Related, What did the 'turbo' button actually do? – user15061 Aug 22 at 11:58
125

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.)

  • 10
    I've still got at least one customer using a patched TP application. They were going to switch to another vendor's Windows application years ago - but that turned out to be vaporware so they stuck with my DOS stuff and still use it to a limited extent today. – manassehkatz Aug 21 at 13:34
  • 4
    I still consider it an irony that run time error 200 was triggered by a CPU above 200 MHz. As if they wanted to tell the critical frequency right in the error message. :-) – celtschk Aug 21 at 15:05
  • 3
    J R Stockton’s page on the topic (ss64.net/merlyn/pas-r200.htm) is an amazing resource. Tremendous attention to detail and a ton of effort to track down available resources. – Skeptycal Aug 21 at 16:43
  • 6
    If the CRT unit is included, it will perform all the CRT unit initializations, including the delay calibration, even if delay procedure itself is not used. It does not know at compile time if whether delay procedure is called or not. – Justme Aug 21 at 17:48
  • 5
    @phuclv Lazy initialisation wasn't required, so why do it? The cost wasn't trivial back then. It was much easier and more convenient to just do it in unit initialisation (note that he problem only happens if you link the CRT unit). In the end, would you really prefer the program to crash on its first delay rather than right at the start? – Luaan Aug 21 at 19:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.