The following article was printed in issue 1 1987 of the magazine „CT".
Debugging TURBO Pascal programs.
|
|
Auf die Schliche
Variablen-Tracer für Turbo-Pascal
Christoph Meyer
Wer hat bei Turbo-Pascal nicht schon einen Tracer vermißt, der nach jeder ausgeführten Anweisung Informationen über den aktuellen Inhalt von Variablen liefert.
Umständliches Einbauen von Write-Anweisungen hätte man sich damit sparen können, denn der Fehler liegt meistens da, wo man ihn zuletzt vermutet.
Aber auch gegen diesen Mangel ist ein Kraut in Form eines Turbo-Programms gewachsen
Der vorgestellte Tracer kann Variablenwerte auf die Einhaltung bestimmter Bedingungen überprüfen und sogar die Stelle in der Source ermitteln, an der die Bedingung verletzt wird.
Immer wieder gibt es Probleme mit Variablen.
Teils sind sie uninitialisiert, oder sie überschneiden sich mit namensgleichen Variablen aus dem Hauptprogramm, oder sie nehmen trotzig Werte an, die sie eigentlich gar nicht annehmen können oder dürfen.
Aber der Turbo-Compiler besitzt ja noch die interessante Direktive U.
Der Effekt dieses Compiler-Schalters ist normalerweise folgender:
Nach jedem kompilierten Pascal-Statement fügt Turbo einen RST38-Opcode
[Siehe Hinweis [1] für JOYCE]
in das Compilat ein.
Dieser Opcode bewirkt bekanntermaßen einen Software-Interrupt zur Adresse $38.
Dort steht ein Sprung direkt in die Turbo-Runtime-Library, also etwa 'JP xxxxh', der nichts anderes bewirkt, als die Tastatur auf ein anliegendes Zeichen hin zu überprüfen.
War es ein Ctrl-C, bricht Turbo die Programmausführung ab.
Das Positive an diesem RST38 ist, daß nach jedem Pascal-Statement etwas Definiertes passiert.
Was hindert uns also daran, die angesprungene Tastatur-Routine durch eine eigene zu ersetzen?
Richtig, nichts!
Der erste Versuch ist, den Jump an der Adresse $38 zu einer eigenen Routine umzulenken, die meinetwegen 'Hallo' ausgibt.
Gedacht, getan.
Aber dann kam die große Enttäuschung: der Rechner hängt sich heilos auf, nichts geht mehr.
Die Ursache war schnell gefunden.
Im Compilat steht eben nach jedem Statement ein RST38, also auch in der Hallo-Routine.
Damit entsteht eine perfekte, endlose Rekursion, die uns dem Ziel natürlich keinen Schritt näher bringt.
Nach dem Einrahmen der Hallo-Routine mit einem {$U-} und einem {$U+}, wird diese nicht mehr mit RST38-Opcodes versehen, und alles klappt wie gewünscht.
Das Programm gibt jetzt nach jedem ausgeführten Statement die beiden Hallos aus, womit schon der wichtigste Schritt hin zu einem Tracer getan ist.
Harte Forderungen
Was jetzt noch zum Variablen-Tracen fehlt, ist leicht aufgezählt und ebenso schnell programmiert.
Es sollen alle Veränderungen einer Variablen auf dem Bildschirm angezeigt werden, und das nach Möglichkeit, ohne den normalen Bildschirmaufbau zu stören.
Natürlich muß man der Variablen auch während des Programmlaufs von der Tastaur aus einen neuen Wert zuweisen können.
Und schick wäre es auch, wenn das Programm laufen würde, bis eine Variable einen ganz bestimmten Wert angenommen hat, und dann abbrechen würde.
Und das bitte alles in allen Kombinationen!
Diese Wünsche waren schnell erfüllt.
Aber zufrieden war ich noch nicht.
Was mir fehlte, war so etwas wie die Source zu der Stelle, an der eine Variable einen bestimmten Wert annimmt.
Sonst ist die Fehlersuche in der Source doch wieder recht schwer, da man dann immer noch nicht weiß, mit welchem Befehl diese Zuweisung zustandekam.
Als Vorbild kann hier nur wieder Turbo selber dienen.
Wenn in einem im RAM laufenden Programm ein Fehler auftritt, sucht Turbo eigenständig nach dem fehlerhaften Quelltext.
So etwas müßte man auch beim Tracen können.
Aber das war lange Zeit ein ungelöstes Problem, und ich behalf mich mit dem Tracen von Turbo-Programmen auf Assembler-Ebene mit einem herkömmlichen Debugger.
Aber auch dieses Problem blieb nicht ungelöst.
Nach jedem ausgeführten RST38-Jump befindet sich oben auf dem Stack die Adresse des auszuführender Befehls nach Abarbeitung der RST-Routine.
Wenn die Turbo-Runtime-Routinen nun einen Fehler entdecken, so wird ebenfalls die Adresse, an der der Fehler im Compilat auftrat zuoberst auf dem Stack abgelegt.
Danach lädt der Compiler eine Fehlernummer in den Akku und springt zur Adresse $2027 (Turbo 3.0).
Diese Routine zeigt die übliche Fehlermeldung mit Fehlernummer auf dem Bildschirm und veranlaßt dann ein Neukompilieren der Source, bis die auf dem Stack liegende Adresse im Compilat erreicht ist.
Dann wird nach <ESC> die entsprechende Stelle in der Source exakt angezeigt.
Diesen Mechanismus kann man mit ein paar Zeilen Inline-Code selbst nachvollziehen.
Dazu muß dem Tracer bei der Anzeige eines neuen Wertes mitgeteilt werden, daß nun die Stelle im Source interessiert und diese doch bitte angezeigt werden soll.
Um die Sache klarer zu machen, übergibt der Tracer die sonst nicht existierende Runtime-Fehlernummer $33 und leitet die oben beschriebene Prozedur ein.
Daraufhin erscheint wirklich die richtige Stelle in der Source!
Damit sind jetzt alle wichtigen Teile des Tracers beschrieben.
Voll einbindbar
Jetzt zum Programm selber.
Die abgedruckten Routinen sind als Include in jedem anderen Programm einbindbar.
Die einzige Bedingung ist die Verwendung von Turbo 3.0.
Die Funktion der benutzten Prozeduren ist leicht zu durchschauen, deshalb gebe ich nur zu einigen Variablen, Typen und Prozeduren kurze Erläuterungen.
Die Typvereinbarung 'db_types' gibt die im Augenblick von dieser Version des Debuggers unterstützten Variablentypen an.
Dies läßt sich nach eigenem Belangen erweitern, muß aber auch an anderen Stellen des Programms berücksichtigt werden.
Der Typ 'db_compares' zeigt die möglichen Vergleiche, die bei Bedingungen für das Tracen dann zu einem bestimmten Zustand möglich sind.
Dabei steht eq für 'equal', lt für 'less than' und so weiter.
Die Variablen trace_int, trace_byt,... speichern den Wert, der für die Vergleichsoperationen verwendet werden soll.
Da immer nur ein Variablentyp getraced werden kann, ist auch nur eine von diesen Variablen von Bedeutung.
Von den internen Variablen seien hier nur einige genannt.
In 'oldrst38' wird die Originaladresse der RST38-Routine für späteren Gebrauch aufbewahrt und in 'calling' die Rückkehradresse zum Beenden der Interrupt-Routine.
Die Prozeduren 'hexbyte' und 'hexinteger' benutzen Runtime-Routinen von Turbo 3.0, um Bytes und Integers auf einfache Weise in hexadezimaler Form auszugeben.
Die clear-Routine löscht eine Ausgabe des Tracers wieder vom Bildschirm, indem sie eine Folge von Backspaces ausgibt, die der Länge der Ausgabe entspricht.
'getnewval' liest je nach getractem Typ einen neuen Wert für die Variable ein.
Dabei kann man bei Bytes und Integers auch hexadezimale Zahlen mit vorangestelltem '$' eingeben.
Für boolesche Eingaben reichen 'F' und 'T'.
Die Anzeige eines neuen Wertes übernimmt die Prozedur 'show_val', die Bytes und Integers als Hex- und Dezimalzahlen ausgibt und boolesche Werte ausschreibt.
Die Eingaben und Entscheidungen des Benutzers nimmt 'query' entgegen.
Weitere Einzelheiten dazu später.
Wenn Vergleiche anstehen, übernimmt 'db_compare' diese Aufgabe.
Die Installierung des Tracers auf eine Variable und die Initialisierung aller wichtiger Variablen vollzieht sich in 'debugger_init'.
Debugging Inline
Die eigentliche Behandlung des RST38 geschieht in der Prozedur 'debugg', die auf keinen Fall auf irgendeine Weise von Pascal aus aufgerufen werden darf.
Dort werden zu Beginn die Return-Adresse und alle Register gerettet.
Dann stellt die Routine fest, ob sich die betreffende Variable seit dem letzten Statement verändert hat, und zeigt gegebenenfalls, je nach eingestellten Features, den neuen Wert an, weist der Variablen einen neuen Wert zu oder unterbricht den Programmablauf, um das aktuelle Statement in der Source anzuzeigen.
Wenn nicht unterbrochen werden soll, bringt 'debugg' den Stack wieder in Ordnung, und das Programm nimmt seinen normalen Lauf.
Die Routinen 'debugger_on' und 'debugger_off' dienen zum Ein- und Ausschalten des Debuggers.
Erst ein Aufruf von 'debugger_on' richtet den RST38-Jump auf die debugg-Routine.
Auf diese Weise kann man gezielt einen Einsatzpunkt für den Debugger bestimmen.
Nun aber zur Bedienung des Tracers.
Wie gesagt, muß man den Tracer nur in sein eigenes Programm einbinden.
Was dann noch zu tun bleibt, ist, lediglich dem Tracer mitzuteilen, welche Variable man tracen möchte, von welchem Typ sie ist und welche Vergleiche stattfinden sollen.
Zum Festlegen dieser Werte dient die Prozedur 'debugger_init'.
Ihre Parameter haben die folgende Bedeutung:
a ist die Adresse der Variablen, die man sich in Turbo mit Addr(Name) besorgen kann.
Natürlich kann die Variable auch ein Element aus einem Array sein.
ty ist der Typ der Variablen, die getraced werden soll.
Die erlaubten Werte hierfür sind im Typ db_types definiert.
c ist der Typ von Vergleich, der zum Programmabbruch führen soll, wenn er zutrifft.
d bestimmt, ob jede Veränderung der Variablen angezeigt werden soll.
Wenn d true ist, zeigt der Tracer den neuen Wert an und wartet dann eine Eingabe durch den Anwender ab.
Hierfür stehen folgende Kommandos zur Verfügung:
Mit <S> kann man den aktuellen Wert der Variablen neu setzen.
Der Tracer fordert dann zur Eingabe auf.
Wie bereits erwähnt, kann man für Byte- und Integer-Variablen auch eine hexadezimale Eingabe mit vorangestelltem '$' wählen.
Für eine boolesche Variable reicht 'T' oder 'F'.
<Q> heißt 'Quiet', der Debugger bleibt zwar aktiv, macht aber keine weiteren Ausgaben, und man kann die Variable natürlich nicht mehr von Hand verändern.
Die interessanteste Möglichkeit stellt <ESC> dar.
Diese Taste bewirkt einen sofortigen Programmabbruch mit dem neu definierten Runtime Error $33.
Danach geht Turbo seinen bei einem Runtime Error normalen Weg.
Es kompiliert die Source bis zu dem Statement, an der der Tracer das Programm unterbrach, und zeigt die Stelle im Editor an.
Wenn das Flag d auf false steht, macht der Tracer keine Ausgaben.
t bestimmt, ob bis zu einer bestimmten Bedingung getraced werden soll oder nicht.
Wenn t true ist, entnimmt der Tracer aus der entsprechenden 'db_...'-Variablen den Trace-Punkt, und der oben beschriebene Parameter 'c' bestimmt den zu verwendenden Vergleich.
So läßt sich zum Beispiel auf eine Integer-Variable tracen, falls sie größer als 100 ist.
In diesem Fall muß 'c' den Wert 'gt' (greater than) haben.
Trifft der Vergleich zu, leitet der Tracer denselben Vorgang wie bei der Eingabe von <ESC> ein.
Nur in dem Fall, daß t true ist, spielt eine der 'db_...'-Variablen eine Rolle.
Diese Variable sollte man dann auch setzen, sonst hat man wieder eine uninitialisierte Variable, und dann...
Die beiden Parameter 'd' und 't' lassen sich nach Belieben kombinieren.
Teilweise ausschließen
Jetzt noch ein paar wichtige Hinweise.
Man kann einzelne Teile der eigenen Source vom Tracen ausschließen, indem man sie genauso wie den Tracer selber mit {$U-} und ($U+} einrahmt.
Diese Teile werden dann vom Tracer nicht beachtet.
Wenn lokale Variablen in Prozeduren zu tracen sind, so sollte die Initialisierung des Tracers erst in dieser Prozedur stattfinden.
Es ist nicht möglich, Werteparameter (Call-by-value-Parameter) von Prozeduren zu tracen, da sie über den Rekursions-Stack verwaltet werden und die Addr-Funktion in solchen Fällen nicht immer exakt arbeitet.
Zum Schluß noch ein paar Worte zu dem kleinen Testprogramm 'Debugger_Test'.
Es ist zwar kurz, zeigt aber sehr eindrucksvoll die Möglichkeiten des Tracers und seine Installation in eigene Programme.
Das Beispiel zeigt den klassischen Plus-Minus-1-Fehler und dessen Aufdeckung durch Tracen eines Schleifenzählers.
Vor allem dann, wenn man viele verschiedene Progammiersprachen benutzt, weiß man manchmal nicht, ob die untere Grenze eines Array 0 oder 1 ist (in C ist sie immer 0, in Pascal je nach Belieben), und schon liefert die Prozedur 'Summe' ein falsches Ergebnis an 'sum' ab.
Man sollte dieses Programm erst einmal ohne den Tracer laufen lassen, dann sieht man es genau
(Hinweis: löschen Sie die Direktive {SU+}).
|
Es gibt praktisch nichts, was sich in Turbo-Pascal nicht machen ließe - wie beispielsweise dieser Tracer für Variablen.
[Listing DEBUGGER.PAS] |
|
Ein nicht selten vorkommender Fehler - der Turbo-Tracer deckt ihn auf.
[Listing TEST.PAS] |
1. |
Im JOYCE wird die RST38-Adresse für Interrupts benutzt, so dass sich hier ein Konflikt ergibt.
Seinerzeit stellte die Firma Heimsoeth einen entsprechenden Patch vor.
Nach dem Aktivieren des Patches muss in der hier vorgestellten Datei
DEBUGGER.PAS die Zeile
rst38 : integer absolute $39;
geändert werden in
rst38 : integer absolute $31;
|
Scanned by
Werner Cirsovius
August 2004
© Heise Verlag