The following article was printed in June 1986 of the magazine „MC".
A self programmed error handler for Turbo Pascal (Version 2.0)
Hans-Jürgen Schmidt

Turbo-Pascal ohne Absturz

Bei zahlreichen mathematischen Pascal-Programmen kann man nur mit sehr großem Aufwand Runtime-Fehler wie Division durch Null oder Gleitkomma-Überlauf vermeiden. Manchmal sehnt man sich richtig nach den „ON-ERROR"-Anweisungen vieler Basic-Interpreter zurück. Nach eingehender Analyse der CP/M-Turbo-Pascal-Fehlerbehandlung wurde ein Weg gefunden, durch einen eigenen Error-Handler Runtime-Fehler abzufangen1.

In Turbo-Pascal gibt es zwei Arten von Fehlern, die zum Abbruch eines Programms führen können: I/O-Errors und Runtime-Errors. Mit der Funktion IORESULT und den entsprechenden Compiler-Directiven wird dem Benutzer eine eigene Behandlung von I/O-Errors ermöglicht. Bei Runtime-Errors ist eine Fehlerbehandlung durch den Benutzer nicht vorgesehen.

Zur Analyse des Turbo-Pascal-Error-Handlers wurde der entsprechende Teil der Runtime-Library disassembliert (Bild 1). Bei einem Runtime-Fehler wird das Label RUNERROR angesprungen.
CONIN   EQU     000A3H
CONSTS  EQU     000A0H
IOERR   EQU     000D9H
L00D5   EQU     000D5H
L00D7   EQU     000D7H
L0100   EQU     00100H
PRADR   EQU     0048DH
PRHEX   EQU     00492H
PRSTR   EQU     00225H
RESTART EQU     000DFH

        ORG     01F35H

IORESULT:                       ;01F35H
        LD      HL,IOERR        ;000D9H
        LD      A,(HL)
        LD      (HL),0
        LD      L,A
        LD      H,0
        RET

CTRLC:                          ;01F3FH
	CALL    CONSTS          ;000A0H
        LD      A,H
        OR      L
        RET     Z
        CALL    CONIN           ;000A3H
        LD      A,L
        CP      3
        RET     NZ

USERBRK:                        ;01F4CH
        POP     IX
        CALL    PRSTR           ;00225H
        DB      '^C',0DH,0AH
        DB      'User break',0
        JR      PRPC            ;01F93H



Bild 1. Der Error-Handler von Turbo-Pascal 2.0
IOERROR:     	                ;01F62H
        LD      A,(IOERR)       ;000D9H
        OR      A
        RET     Z
        POP     IX
        PUSH    AF
        CALL    PRSTR           ;00225H
        DB      0DH,0AH,'I/O',0
        JR      PRERROR         ;01F85H

RUNERROR:                       ;01F75H
        POP     IX
        CALL    PRSTR           ;00225H
        DB      0DH,0AH,'Run-time',0
PRERROR:                        ;01F85H
        CALL    PRSTR           ;00225H
        DB      ' error ',0
        CALL    PRHEX           ;00492H
PRPC:                           ;01F93H
        CALL    PRSTR           ;00225H
        DB      ', PC=',0
        PUSH    IX
        POP     HL
        LD      DE,(L00D5)      ;000D5H
        OR      A
        SBC     HL,DE
        LD      DE,15H
        ADD     HL,DE
        LD      (L00D7),HL      ;000D7H
        CALL    PRADR           ;0048DH
        CALL    PRSTR           ;00225H
        DB      0DH,0AH,'Program aborted',0

HALT:                           ;01FC5H
        LD      HL,(RESTART)    ;000DFH
        JP      (HL)

START:                          ;01FC9H
        LD      SP,L0100        ;00100H
;       ...etc
[Hier der abweichende Error-Handler von Turbo-Pascal 3.0]

Der Akku enthält dabei die Fehlernummer. Auf dem Stack findet man noch die Rücksprungadresse des Programmteils, der den Fehler auslöste.

Bild 2 zeigt die Routinen zur benutzerspezifischen Fehlerbehandlung. In der Prozedur SET_ERROR_HANDLER (Zeile 76) wird bei der Adresse RUNERROR ein „JUMP ERROR_HANDLER" eingetragen, nachdem der Original-Code in RUN_TIME_ERROR_BAK gerettet wurde (SWITCH = ON = true). Durch Restaurieren mit dem Original-Code kann der ERROR_HANDLER wieder abgeschaltet werden (SWITCH = OFF = false).
14   {*  einschalten : SET_ERROR_HANDLER (ON);      *}
15   {*    Fehlerbehandlung durch ERROR_HANDLER.    *}
16   {*    Programm laeuft weiter. Potentielle      *}
17   {*    Fehler koennen mit ERROR_NUM abgefragt   *}
18   {*    werden.                                  *}
19   {*  ausschalten : SET_ERROR_HANDLER (OFF);     *}
20   {*    Run-Time-Fehler fuehren zum Programm-    *}
21   {*    abbruch. (Default)                       *}
22   {*                                             *}
23   {*  Function ERROR_NUM :                       *}
24   {*    0 : kein Fehler,                         *}
25   {*    sonst TURBO PASCAL Fehlernummer          *}
35   const
36     ON = true;
37     OFF = false;
38
39   var
40   ERROR_CODE : byte;
41
42
43   procedure WARM_START;        { restart program }
44   begin
45     inline ($C3/$00/$01);      { JP $0100 }
46   end; { WARM_START }
47
48
49   function ERROR_HANDLER : real;
50   begin
51     inline ($32/ERROR_CODE);   { LD (ERROR_CODE),A }
52     ERROR_HANDLER := 0.;
53     case ERROR_CODE of
54     $01 : ;           { Floating point overflow }
55     $02 : ;           { division by 0 }
56     $03 : ;           { sqrt argument < 0 }
57     $04 : ;           { ln argument <= 0 }
58     $10 : ;           { string length error }
59     $11 : ;           { invalid string index }
60     $90 : ;           { invalid array index }
61     $91 : ;           { invalid scalar/subrange }
62     $92 : ;           { out of integer range }
63     $FF : WARM_START; { heap/stack collision }
64     else
65       writeln (' unknown run-time error ', ERROR_CODE);
66       halt;
67     end;
68   end; { ERROR_HANDLER }
69
70   function ERROR_NUM : byte;
71   begin
72     ERROR_NUM := ERROR_CODE;
73     ERROR_CODE := 0;
74   end;
75
76   procedure SET_ERROR_HANDLER (SWITCH : boolean);
77   const
78     JUMP = $C3;
79   type
80     JUMP_VECTOR = record
81                     OPCODE : byte;
82                     ADR : integer;
83                   end;
84   var
85     RUN_TIME_ERROR : JUMP_VECTOR absolute $1F75;
86     RUN_TIME_ERROR_BAK : JUMP_VECTOR;
87   begin
88     if SWITCH then
89     begin
90       ERROR_CODE := 0;
91       RUN_TIME_ERROR_BAK := RUN_TIME_ERROR;
92       RUN_TIME_ERROR.OPCODE := JUMP;
93       RUN_TIME_ERROR.ADR := addr (ERROR_HANDLER);
94     end
95     else RUN_TIME_ERROR := RUN_TIME_ERROR_BAK;
96   end;
Bild 2. Der Runtime-Error-Handler macht bei Rechenoperationen zahlreiche Einzelüberprüfungen überflüssig
[Hier als Listing]
Bei TURBO Pascal 3.0 muss die Zeile 85 geändert werden in:
85     RUN_TIME_ERROR : JUMP_VECTOR absolute $2029;

Der ERROR-HANDLER (Zeile 49) wurde als Real-Funktion deklariert, da es hier im wesentlichen auf Fehler bei Real-Arithmetik ankommt. Die Fehlernummer, die im Akku übergeben wird, muß ganz am Anfang der Routine mit einem Inline-Statement in eine Variable (ERROR_CODE) gerettet werden. In einer Fallunterscheidung (CASE ERROR_CODE) können die einzelnen Fehlertypen unterschiedlich behandelt werden. Hier sind fast nur Leer-Statements eingetragen. Nach Bedarf können z.B. Klartext-Fehlermeldungen ausgegeben werden. Bei den Fehlern $01 und $02 kann man auch mit der Zuweisung „ERROR_HANDLER := 1E38" mit „fast unendlich" weiterrechnen lassen. Eine weitere Möglichkeit ist das Führen einer Statistik, wann und wie oft welche Fehler aufgetreten sind.

In Fällen, in denen ein Weiterlaufen des Programms nicht sinnvoll erscheint, kann man es mit HALT verlassen oder mit WARM_START nochmals von vorne ablaufen lassen, wie z.B. bei einer Heap-Stack-Kollision (Zeile 63).

Als Analogon zur Funktion IORESULT bei I/O-Fehlern dient die Funktion ERROR_NUM bei Runtime-Fehlern. Sie liefert den zuletzt aufgetretenen ERROR_CODE (0 = kein Fehler) und setzt ihn zurück. Man muß also bei einem komplexen arithmetischen Ausdruck nicht vor jeder einzelnen Operation eine potentielle Fehlerbedienung abprüfen (Divisor = 0 ?, ln mit negativem Argument ?, führt Argument von exp zu Overflow ? usw.). Es genügt, nach der Abarbeitung des komplexen Ausdrucks einmal ERROR_NUM auf 0 zu testen.

Bild 3 demonstriert die Anwendung in einem Programmauszug. Hier wird auch gezeigt, daß bei einem WARM_START bisher ermittelte Daten erhalten bleiben können. Zu diesem Zweck wird die Variable FIRST_TIME als „typed constant" mit dem Wert „true" deklariert (Zeile 20). Nach dem Laden des Programms wird im if-Statement in Zeile 150 der then-Block ausgeführt. In diesem Block wird FIRST_TIME auf „false" gesetzt, die Initialisierung der Datenstrukturen vorgenommen und der ERROR_HANDLER aktiviert. Bei einem Runtime-Fehler, für den im ERROR_HANDLER die Prozedur WARM_START aufgerufen wird, beginnt das Programm von vorne. Da nun FIRST_TIMEfalse" ist, wird im if-Statement der else-Block ausgeführt. Da die Datenstrukturen nicht wieder initialisiert werden, bleibt ihr Wert. Durch einen Dummy-Aufruf von ERROR_NUM wird ERROR_CODE gelöscht.
Bild 3. Dieser Programmausschnitt demonstriert, wie bei einem Warmstart die Daten erhalten bleiben
           •
           •
           •
 17  {$I ERROR.PAS}  {$C-,I-}
 18
 19  const
 20    FIRST_TIME : boolean = true;

           •
           •
           •

146
147  begin
148    clrscr;
149    { Initialisierung }
150    if FIRST_TIME then
151    begin  { Initialisierung bei Programmstart }
152      FIRST_TIME := false;
153      SET_ERROR_HANDLER (ON);
154      STACK_PTR := 0;
155    end
156    else
157    begin { warmstart }
158      A := ERROR_NUM; { Fehler  zurücksetzen }
159      writeln;
160      writeln(BELL, 'Warmstart   done.');
161	end;
162
163     { main loop }
           •
           •
           •

1. Bei anderen Compilern lassen sich Fehler-Routinen einfacher implementieren, hier am Beispiel des Pascal MT+ Compilers von Digital Research und des Pro Pascal Compilers von Prospero Software (Auszug aus den Handbüchern):
Pascal MT+ Compiler
4.6.3  User-supplied Handlers

     You can write your own @ERR routine instead of using the system
routine.  Declare the routine as follows:

       PROCEDURE @ERR(ERROR:BOOLEAN; ERRNUM:INTEGER);

     Your version of @ERR should check the ERROR variable and exit
if it is FALSE.  If the value is TRUE, you can decide what action to
take.

     To use @ERR instead of the routine in PASLIB, link your routine
ahead of PASLIB to resolve the references to @ERR.  The values of
ERRNUM are in Table 4-2.
Pro Pascal Compiler
9.3.5  ownerr

This procedure is provided to enable the user to perform bis own
exception handling, as an alternative to the normal reporting of
run-time errors. The procedure ownerr installs a procedure nominated
by the user as his error handler, which will then receive control in
the event of any error arising. The handler is invoked for all types
of error, but has the option of processing some and leaving the others
to be reported at the console in the usual way. The handler must be
written to the parameter specification shown below in the declaration
of ownerr.

        PROCEDURE ownerr
               (PROCEDURE handler (errorletter: char;
                                   erroraddress: integer;
                                   VAR errorstring: string;
                                   fatal: boolean;
                                   VAR processed: boolean)
               ); EXTERNAL;

The procedure nominated as the error handler when ownerr is called
must be at the outer level. (It can itself be an EXTERNAL, for
example in a library.) Its parameters must agree with the list above,
where the purpose of the first four is to provide "handler" with the
information from the standard error message - letter, address,
supplementary string (which may be empty), and fatal/recoverable flag.
The fifth ("processed") is an inout parameter defaulted to false. If
handler leaves this as it is, then on exit the normal report will be
produced; if it is set to true, reporting will be skipped.

Scanned by Werner Cirsovius
Februar 2005
© Franzis' Verlag