← ST-Computer 03 / 1992

Compiler-Optionen des GFA-BASIC-Compilers

Grundlagen

Legt man Wert auf absturzsichere Programme, und/ oder möchte man die neuen Interrupt-Möglichkeiten des GFA-BASIC 3.x nutzen, muß ein Compiler-Lauf mittels Software-Schalter (Optionen) gewissenhaft gesteuert werden, sonst laufen die Programme nicht einwandfrei. Mit dem Handbuch allein tut man sich jedoch oft schwer.

Der Compiler des GFA-BASIC V3.x reagiert auf verschiedene Software-Schalter, die vom BASIC-Programmierer im GFA-BASIC-Sourcecode mittels EinfĂŒgung von z.B. ‘$E+’ geschaltet werden können. Der Interpreter ĂŒberliest bei der ProgrammausfĂŒhrung diese Umschaltungen, im Compiler sollen die Schalter in der Hauptsache dazu dienen, dem Programmierer die Entscheidung zwischen kurzem, schnellem oder sicherem Code zu ĂŒberlassen.

Das Handbuch sorgt durch Beschreibung der Schalter (bzw. Optionen) sowie durch Beispielprogramme dafĂŒr, sie effektiv und vor allem richtig anwenden zu können. Leider wird dabei aber versĂ€umt, auf die Optionen $U, $P und $I im Zusammenhang mit den Interrupt-Befehlen ‘EVERY xxx GOSUB proc’ und der Fehlerbehandlung durch ‘ON ERROR GOSUB proc’ genĂŒgend tief einzugehen.

Bekanntlich fĂŒhren sĂ€mtliche BASIC-Fehlermeldungen - z.B. auch ein ganz banales ‘Diskette voll’ - zum Programmabbruch, sofern nicht explizit eine Fehlerbehandlung durchgefĂŒhrt wird. Man sieht, Fehlerbehandlung ist kein Luxus-Feature fĂŒr Leute, die es besonders schön machen wollen.

Wichtig ist das grĂŒndliche Wissen um die Compiler-Optionen deshalb, weil das Nichtsetzen bzw. falsch Setzen nicht etwa nur einen etwas lĂ€ngeren oder langsameren Code zur Folge hat, das Programm wird vielmehr mit dem Auftreten bestimmter Situationen (Timing, Fehler) seine eigenen Wege gehen. Busfehler weit entfernt von der Ursache des eigentlichen Fehlers (zeitlich wie örtlich) als auch mysteriöse rekursive Prozeduraufrufe grĂŒĂŸen dann mit einem fröhlichen „viel Spaß beim Entwanzen“.

Es folgt die Beschreibung der Optionen. Ein praxisorientiertes Kochrezept, wann welche Schalter gesetzt bzw. nicht gesetzt sein mĂŒssen, folgt am Ende.

Schalter $P...

... wirkt sich beim Einstieg in eine Prozedur und beim Verlassen selbiger aus. Mögliche Schalterstellungen sind $P> und $P<. Bei Prozeduraufrufen muß sich die aufgerufene Routine die Programmstelle merken, die nach dem RETURN als nĂ€chstes bearbeitet werden soll. Mit $P< entscheidet man sich fĂŒr die Hinterlegung dieser Adresse auf dem Stack. NatĂŒrlich muß dann wĂ€hrend der gesamten Routine der Stackpointer (Prozessorregister a7) streng behĂŒtet werden. Mit $P> wĂ€hlt man die Ablage auf dem Heap. Es gilt im Grunde das gleiche wie eben, der Heappointer (Prozessorregister a3) darf nicht verlorengehen.

Der Heappointer kann wĂ€hrend des Programmlaufs auf Kosten von AusfĂŒhrungsgeschwindigkeit und CodelĂ€nge wesentlich sorgsamer behandelt werden als der Stackpointer. Tritt wĂ€hrend einer Bibliotheksfunktion ein Fehler auf (Division durch Null, Überlauf), gerĂ€t der Stackpointer meist außer Kontrolle. Dies lĂ€ĂŸt man sich allerdings gefallen, weil der Stack vom Prozessor stĂ€ndig benutzt wird und deshalb das Sichern des Stackpointers massig Zeit und Code kosten wĂŒrde. In 99.99% der FĂ€lle wĂ€ren solche pauschalen Vorsorgemaßnahmen sowieso fĂŒr die Katz.

Die Compiler-Philosophie muß deshalb sein, nur bei Bedarf die Ablage aller lebensnotwendigen Informationen zu veranlassen, mit denen sich das Programm im Fehlerfalle wieder in geregelte Bahnen lenken lĂ€ĂŸt.

Hier tritt der Grund fĂŒr die Einbeziehung des Programmierers in den Compi-liervorgang deutlich zutage: Er kennt die fehlertrĂ€chtigen Programmstellen und erhĂ€lt deshalb die Erlaubnis, das Sichern dieser Informationen gezielt ‘von Hand’ einzufĂŒgen - das Programm lĂ€uft dann zeitweise (oder ganz) mit Netz und doppeltem Boden.

Die P-Option stellt eine dieser Programmiererfreiheiten dar. Nebenbei: Falls in der Prozedur lokale Variablen benutzt werden, ‘overruled- der Compiler den Programmierer und benutzt immer die $P>-Version.

Schalter $U...

... ist wichtiger als die P-Option, da sie zwischen allen codeerzeugenden BASIC-Befehlen eingesetzt werden kann. Mit $U+ veranlaßt man die wiederholte EinfĂŒgung des Maschinenbefehls ‘jsr(a6)’ (jsr=jump subroutine = bearbeite Unterprogramm) vor jedem BASIC-Befehl bis zum Abschalten der Option durch $U-. Möchte man nur einzelne ‘jsr (a6)’ einfĂŒgen, verhindert man die automatische EinfĂŒgung mit ‘$U-’ und schreibt gezielt ‘$U’ an den relevanten Stellen.

Der Compiler erzeugt eine Tabelle im Datenbereich des Programms, die ĂŒber die Adressen aller ‘jsr (a6)’ im Programm Buch fĂŒhrt. Jeder Eintrag in dieser Tabelle bezeichnet dabei den Adreßabstand zum VorgĂ€nger - $U als vorzeichenloses Wort (erster Eintrag = Programmstart bis erstes $U). Die Tabelle kommt bei Abarbeitung der ‘RESUME NEXT’-Anweisung zum Einsatz.

Zwischen den Zeilen

Was leistet nun das kleine Unterprogramm ‘jsr (a6)’? Die Prozessorregister Stackpointer (a7), Heappointer (a3) und ProgrammzĂ€hler (pc) werden in einen speziellen Speicherbereich kopiert und damit die EintrĂ€ge des letzten Aufrufs ĂŒberschrieben. Der genannte pc enthĂ€lt die Adresse des Maschinenbefehls nach ‘jsr (a6)’, also die Anfangsadresse der dem $U folgenden BASIC-Zeile.

Ist dies erledigt, kehrt die Routine sofort zurĂŒck, es sei denn, es wurde die Interrupt-Option $I+ eingeschaltet. Dann wird die Tastenkombination Alt-Shift-Ctrl getestet und, falls eine EVERY ...- bzw. AFTER ...-Routine aktiviert ist, die Zeit geprĂŒft. Bei ErfĂŒllung der Zeitbedingungen zur Bearbeitung einer solchen Routine wird direkt in diese verzweigt.

BehĂ€lt man den Umstand im Auge, daß dieses Unterprogramm bei entsprechender Compiler-Steuerung nach jeder BASIC-Zeile erneut ausgefĂŒhrt wird, erwartet man eine deutlich spĂŒrbare Verlangsamung. Die Compiler-Bauer bemĂŒhten sich daher hier ganz besonders um extrem schnellen Maschinencode. Trotzdem lohnt es sich, bei laufzeitintensiven Schleifen die $U-Aufrufe per Hand auszudĂŒnnen.

Ich möchte herausheben, daß die kontinuierliche Abarbeitung einer Interrupt-Routine mittels EVERY ... unbedingt des Setzens der $U-Aufrufe in zeitlich nicht allzu weit entfernten, regelmĂ€ĂŸigen AbstĂ€nden bedarf.

TatĂŒtata!

Erkennt eine Bibliotheksfunktion einen Laufzeitfehler wie z.B. ‘Logarithmus von 0’, so darf sich eine weitere Bibliotheksfunktion bewĂ€hren, nĂ€mlich die interne Fehlerroutine ‘ERROR’.

Dieser Maschinenroutine fĂ€llt es zu, die durcheinandergeratenen Register wieder auf die Reihe zu kriegen. Abschließend verzweigt ‘ERROR’ zur Fehlerbehandlungsroutine, mit der Anweisung ‘ON ERROR GOSUB errproc’ hoffentlich voreingestellt (falls nicht: Programmabbruch). Am Ende der Fehlerbehandlungsroutine sollte die Anweisung RESUME... stehen. WĂ€hrend der Bearbeitung dieser Anweisung geschieht folgendes:

Der Stack wird - egal ob RESUME, RESUME NEXT oder RESUME Label - vollstĂ€ndig zurĂŒckgesetzt, ohne daß die auf ihm gespeicherten Daten besonders ausgewertet wĂŒrden. Mit dem Heap wird wesentlich dezenter verfahren, er wird korrekt bis zur richtigen Höhe abgearbeitet.

Der bei der letzten $U-Routine gespeicherte pc erhĂ€lt durch den RĂŒcksprung mittels RESUME bzw. RESUME NEXT eine besondere Bedeutung. Bei RESUME wird dieser pc als Wiedereinstiegsadresse herangezogen, bei RESUME NEXT wird anhand der oben erwĂ€hnten Tabelle die nĂ€chste $U-Routine gesucht. Das geschieht so: Auf die Startadresse des Programms werden solange die einzelnen EintrĂ€ge dieser Tabelle aufaddiert, bis diese akkumulierte Adresse grĂ¶ĂŸer ist als der gespeicherte pc. Diese Adresse wird als neuer pc eingesetzt.

Das Kochrezept

Die obigen AusfĂŒhrungen ziehen einige Konsequenzen nach sich, die unbedingt beachtet werden mĂŒssen:

  • Weil die Wiedereintrittsadresse in das regulĂ€re Programm erst nach der Fehlerprozedur, nĂ€mlich mit der Anweisung RESUME..., gebildet wird, darf die Fehlerprozedur selbst sowie sĂ€mtliche von ihr aufgerufenen Prozeduren und Funktionen nicht einen einzigen $U-Aufruf enthalten. Dies wĂŒrde den pc ĂŒberschreiben - es sollte nun klar sein, wo eine RESUME-Anweisung in diesem Fall hinfĂŒhrt (Demoprogramm 1).
  • Als Folge daraus ist auch klar, daß in einer Fehlerroutine kein weiterer Fehler auftreten darf.
  • Ein RESUME NEXT fĂŒhrt das Programm nicht in der nĂ€chsten BASIC-Zeile weiter, sondern mit dem nĂ€chsten $U-Aufruf. Es ist zu bedenken, daß dadurch nicht abgearbeitete Befehlszeilen Folgefehler nach sich ziehen können. FĂŒr RESUME gilt das gleiche: Nicht die selbe Zeile wird erneut ausgefĂŒhrt, sondern exakter die Zeilen nach dem (zeitlich) letzten $U-Aufruf (Demoprogramm 1).
  • Die Option $P< darf nur bei solchen Prozeduren angewendet werden, in denen mit Sicherheit kein Fehler auftritt, d.h. der gesamte ‘Prozedurbaum’, der in dieser Routine wurzelt, darf keine Fehlerbehandlung erleiden. Der Grund liegt darin, daß die RĂŒcksprungadresse auf dem Stack hinterlegt wird, die RESUME-Bearbeitung den Stack jedoch verwirft. Zuwiderhandlungen werden frĂŒher oder spĂ€ter (meistens spĂ€ter) mit Bomben bestraft. (Dies gilt, wie oben erwĂ€hnt, nur fĂŒr Prozeduren ohne lokale Variablen) (Demoprogramm 2).
  • Die Routinen fĂŒr EVERY ... und AFTER ... dĂŒrfen keine $U-Aufrufe enthalten. Falls doch $U-Aufrufe Vorkommen, hĂ€tte dies zwei Folgen:

Zum einen, angenommen, der $U-Aufruf vor einer fehlererzeugenden BASIC-Zeile wĂŒrde die EVERY-Prozedur aktivieren. Die $U-Aufrufe innerhalb der EVERY-Prozedur wĂŒrden dann den pc wieder und wieder ĂŒberschreiben. Nach RĂŒckkehr aus der EVERY-Prozedur bis zur fehlererzeugenden Zeile erfolgt jedoch kein weiterer $U-Aufruf. Ein RESUME NEXT hat die ProgrammfortfĂŒhrung nach der EVERY-Prozedur zur Folge.

Zum anderen wĂŒrde in der Interrupt-Routine eine erneute PrĂŒfung der EVERY-Bedingung vorgenommen. Da keine Semaphor-Variable den erneuten Aufruf der EVERY-Prozedur verhindert, kann es bei entsprechend langen Bearbeitungszeiten zu rekursiven Aufrufen der Interrupt-Routine kommen. Im Extremfall beschĂ€ftigt sich das Programm nur noch mit dieser Prozedur (Demoprogramm 3).

Ich hoffe, der GFA-Gemeinde mit den obigen AusfĂŒhrungen einige leidvolle Erfahrungen zu ersparen.

' Testprogramm 1 ' ' Demonstration von $U+ innerhalb der ' Fehlerabfangroutine. ' $U+,$P> ' ON ERROR GOSUB errproc ' PRINT "Programmstart" REPEAT UNTIL INKEY$<>"" ' a=200/0 ' PRINT "Prommende" REPEAT UNTIL INKEY$<>"" END ' $U- PROCEDURE errproc ALERT 1, "Fehler "+STR$(ERR)+"|aufgetreten",2,"resume|res next",e& ON ERROR GOSUB errproc IF e&=1 $U PRINT "Endlosschleife einmal anders <Taste>" ' IF INKEY$<>"" END ENDIF ' RESUME ELSE $U RESUME NEXT $U PRINT "falsches RESUME NEXT" ERROR 7 ENDIF RETURN ' Testprogramm 2 ' ' Demonstration von $F< mit nachfolgendem ' Fehler. Die Fehlerroutine scheint einwandfrei ' zu arbeiten, erst spĂ€ter erscheinen unerwartet ' Bomben. ' $U+ ON ERROR GOSUB errproc test_p_kleiner PRINT "Dieser Text wird nie ausgegeben" END ' $P< PROCEDURE test_p_kleiner a_test ' PRINT "Returnadresse fehlt, gleich gibts Bomben <Taste>" REPEAT UNTIL INKEY$<>"" RETURN ' $P> PROCEDURE a_test x%=0 PRINT "fehlererzeugender Aufruf <Taste>" REPEAT UNTIL INKEY$<>"" b_test ' x%=10 PRINT "fehlerfreier Aufruf <Taste>" REPEAT UNTIL INKEY$<>"" b_test RETURN ' $P> PROCEDURE b_test a=c%/x% RETURN ' $U-,$P> PROCEDURE errproc x%=x%+1 ALERT 1,"Fehlerroutine aktiv",1,"res next",e& RESUME NEXT RETURN ' Testprogramm 3 ' ' Demonstration von $U+ in Interruptroutinen ' ------------------------------------------ ' Erzeugt kleines Chaos auf dem Bildschirm ' und lĂ€ĂŸt Fehlerbehandlung ins Abseits laufen. ' Mit den Tasten <i> und <d> kann die Bearbeitungs- ' zeit der Interruptprozedur geĂ€ndert werden, ' beim Selbstaufruf von rupt erfolgt automatisch ' eine VerkĂŒrzung. ' $U+,$I+ $P> ' ON ERROR GOSUB errproc EVERY 5 GOSUB rupt ' l%=2 schleife END ' PROCEDURE schleife DO t$=UPPER$(INKEY$) SELECT t$ CASE "" ' nichts weltbewegendes CASE "E" PAUSE 10 !damit rupt bearbeitet wird ERROR 1 CASE "X" EXIT IF TRUE CASE "I" l%=SUCC(l%) PRINT PRINT "I ";l% CASE "D" l%=MAX(PRED(l%),1) PRINT PRINT "D ";l% DEFAULT EVERY STOP PRINT PRINT "E: Fehler erzeugen" PRINT "I: EVERY PROC verlĂ€ngern" PRINT "D: EVERY PROC verkĂŒrzen" PRINT "X: Programmende" EVERY CONT ENDSELECT LOOP RETURN ' $U+ PROCEDURE rupt DEC l% ! Bearbeitungszeit kĂŒrzer PAUSE 1% ' hier wird EVERY-Bedingung geprĂŒft, ' und lange Pausen ergeben: erfĂŒllt !!! PRINT " A"; ' aber auch hier ... PRINT l%; INC l% ! wieder normal RETURN PRINT PRINT "Guten Morgen" END ' $U- PROCEDURE errproc ALERT 1,"Fehler "+STR$(ERR)+"|aufgetreten",1,"res next",e& ON ERROR GOSUB errproc RESUME NEXT RETURN
Wolfgang Ettig