Mini-Debug: Ein portabler Debugger fuer C-Programme =================================================== Version 1.2 Reinhard Wobst, 6.4.88 1.Allgemeines ------------- Jeder C-Programmierer weiss, wie schwierig die Fehlersuche in etwas komplizierteren C-Programmen ist. Ein Debugger, d.h. ein Hilfsprogramm zur Fehlersuche waehrend der Laufzeit, ist dazu beinahe unerlaesslich, leider aber nicht immer verfuegbar. Diese Luecke versucht Mini-Debug zu schliessen. Es kann nicht mit professionellen Debuggern konkurrieren, wird aber ohne Frage bei Fehlen dieser eine sehr wertvolle Hilfe sein. Das Besondere an Mini-Debug ist seine Portabilitaet. Es besteht aus zwei C-Programmen, die - ev. mit geringem Aufwand geaendert - unter jedem "K&R" - C-Compiler laufen (vgl.6.). Damit ist Mini-Debug nicht mehr an ein bestimmtes System gebunden. Vorausgesetzt werden allerdings einige wenige Faehigkeiten des Terminals, vgl.6. Die Portabilitaet schraenkt natuerlich die Moeglichkeiten des Debuggers ein. Dennoch sind folgende Funktionen verfuegbar: - Anzeigen eines Quelltextausschnitts in einem Fenster und Markieren der gerade abgearbeiteten Anweisung; - Anzeigen von bis zu 10 Variablen gleichzeitig; - Aendern der Werte der angezeigten Variablen; - Aendern des Ausgabeformats fuer diese Variablen; - Setzen von breakpoints (Haltepunkten) unmittelbar im Quelltext; - Unterbrechen des Programms, wenn eine bestimmte Variable einen vorgege- benen Wert erreicht bzw. diesen ueber- oder unterschreitet; - verlangsamter Programmablauf mit gleichzeitiger Beobachtung des Quell- texts auf dem Bildschirm u.a. Zur Arbeitsweise: Im Quelltext sind alle Abschnitte, bei denen der Debugger aktiv werden soll, von bestimmten Kommentaren einzurahmen. Dabei werden die anzuzei- genden Variablen vom Programmierer ausgewaehlt. Der so vorbereitete Quelltext wird durch das C-Programm debug.c modifi- ziert. Es entsteht ein neues C-Programm, das unter Einbindung von trace.c uebersetzt werden muss (dieses etwas umstaendliche Vorgehen ist der Preis fuer die Portabilitaet!). Zur Laufzeit liest das uebersetzte Programm beim Erreichen eines mar- kierten Abschnitts Teile des Quelltexts und springt zum Debugger. Die Laufzeit der Debug-Abschnitte vergroessert sich natuerlich je nach Art des Programms unwesentlich oder erheblich, alle anderen Programm- teile bleiben unbeeinflusst. 2.Einbinden des Debuggers in ein Programm ----------------------------------------- Zunaechst sind debug.c und trace.c zu uebersetzen (debug.c zur lauffaehigen Task mit ca. 1KByte Stack, trace.c zum Objektfile). Danach pruefe bzw. handele man nach folgender Liste: 2.1. Der Quellfile darf keine Syntaxfehler enthalten. Unter RSX muss er vom Typ .C sein. 2.2. Folgende Funktionsnamen duerfen nicht verwendet werden: _trini _trad _tr 2.3. Das Programm kann beliebig viele, sich nicht ueberlappende Abschnitte enthalten, die mit Mini-Debug verfolgt werden sollen (im folgenden Debug-Abschnitte genannt). Ein Debug-Abschnitt muss vollstaendig innerhalb einer Funktion liegen und darf keine Deklarationen oder Praeprozessoranweisungen enthalten. Er muss ueber seine erste Anweisung erreicht werden, d.h., der TRON-Kommentar (s.u.) darf niemals uebersprungen werden! Aufrufe von Funktionen vom Typ 'void' duerfen nicht enthalten sein (s.a.4). 2.4. Ein Debug-Abschnitt wird wie folgt markiert (eckige Klammern zeigen den fakultativen Teil): Beginn: /* TRON [liste] */ Ende: /* TROF */ Als Kennung gilt, dass das erste Wort eines Kommentars (nach ev.Leer- zeichen) TRON bzw. TROF ist. Die Liste, falls sie erscheint, hat fol- gendes Format: format1:l_val1; format2:l_val2; ... Dabei ist 'format' das entsprechende Format der printf/scanf-Funktion, o h n e '%' am Anfang. 'l_val' bezeichnet einen zu dieser Zeit bereits deklarierten L-Wert (im folgendenden oft einfach "Variable" genannt). Der Ausdruck '&l_val' muss sinnvoll und k o n s t a n t sein. Beispiel: /* TRON c:sign; 20.15le:a[3]; -3d:v->count; */ ... /* TROF */ Unzulaessig: /*TRON d:a[i]; */ - a[i] hat keine konstante Adresse! Zu beachten ist, dass doppelt genaue Groessen das Format "le" haben muessen! 2.5. Das uebersetzte debug.c wird gestartet und der Name des modifizierten Quellfiles eingegeben: Unter UNIX in der Form debug filename und unter RSX auf Anforderung. Debug schreibt ein C-Programm trc.c, in dem Funktionsaufrufe eingefuegt, Kommentare weggeschnitten und aufeinanderfolgende Leerzeichen komprimiert werden. Debug fuehrt eine schwache Syntaxpruefung der TROF-/TRON-Anweisungen durch. 2.6. Nun wird trc.c uebersetzt und das uebersetzte Programm trace.c als Objektfile mit eingebunden. Es duerfen keine Syntaxfehler auftreten (vgl.4). 2.7. trc wird gestartet und wie ueblich abgearbeitet. Wird ein Debug- Abschnitt erreicht, so liest trc den Quelltext; dabei laeuft ein Zaehler mit. Danach startet Mini-Debug. Nach Verlassen des Debug- Abschnittes arbeitet das Programm wie gewohnt. Bemerkung zu 2.3.: Wird in einem Debug-Abschnitt eine Funktion gerufen, die ebenfalls Debug-Abschnitte enthaelt, so zaehlt das als unzu- laessige Ueberlappung, denn nach dem "return" wird kein Laden von Quelltext mehr veranlasst. Zulaessig ist nur /*TRON ...*/ ... /*TROF*/ f(a,b,c); /*TRON ...*/ 3.Bedienung ----------- Der Bildschirm ist in 3 Teile eingeteilt: - oben erscheinen 7 Zeilen Quelltext, in denen die aktuelle Anweisung durch ein in "set_mark()" (s.trace.c) festgelegtes Grafikzeichen markiert wird; - in der Mitte werden links bis zu 10 im TRON-Kommentar definierte L-Werte mit laufender Nr., Name, Anzeigeformat und Wert angezeigt und staendig ak- tualisiert, rechts erfolgt die Kommandoeingabe; - Ein- und Ausgabe des Nutzerprogramms erfolgen in der ersten Zeile des un- teren Drittels. Bei Anzeige ist die markierte Anweisung bereits ausgefuehrt worden, der berechnete Wert wird immer auf den Typ 'int' konvertiert und als Integer n a c h Kommandoeingabe an das Nutzerprogramm zurueckgegeben. Dadurch ist es mit dem V-Kommando (s.u.) moeglich, if-, while- und for- Bedingungen zur Laufzeit zu beeinflussen. Die Zeilen werden auf eine Laenge von SCRWDTH (hier:78) Zeichen gekuerzt. In vorliegender Version wird der Cursor nach Kommandoeingabe stets auf die erste Zeile des unteren Bildschirmteils gesetzt; Ein- und Ausgabe des Nutzerprogramms erfolgen also in dieser Zeile (betr. Aenderung vgl.7.). Zur Kommandoeingabe braucht nur eine Taste gedrueckt zu werden, ist nicht notwendig. Gross- und Kleinbuchstaben sind gleichberechtigt. Illegale Kommandos werden mit '???' quittiert. Ein Hilfsmenue ist stets ueber 'H','h' oder '?' erreichbar. Beschreibung der Kommandos: A animation mode Das Programm laeuft wie ein "Trickfilm" ab, so als wuerde jede Se- kunde gedrueckt (s. 'S/'). Unter UNIX wird der Lauf mit der DEL bzw. BREAK-Taste unterbrochen. Unter RSX unterbricht ein beliebiges anderes Kommando die Abarbeitung. Am besten drueckt man eine Taste, die der Monitor ignoriert. B set breakpoint Ein Haltpunkt wird gewaehlt. Es wird die Bewegungsrichtung des Cursors (+/- = vorwaerts/rueckwaerts) angezeigt. Folgende Tasten koennen nun getippt werden: + - Aenderung der Bewegungsrichtung des Cursors vorwaerts/rueckwaerts 1...9 Der Cursor bewegt sich in der gewaehlten Richtung 1...9 Schritte. Ein Schritt ist ein Sprung auf das naechstgele- gene ';' oder ')', auch in Kommentaren, Zeichenketten und -konstanten. Bei Erreichen des Textendes wird der obere Bildschirmteil geloescht. Nach Ruecksetzen des Cursors um 1 Schritt er- scheint wieder die letzte Anweisung. = Der gewaehlte Haltepunkt wird gesetzt. Das Programm arbeitet weiter und haelt erst bei Erreichen des Haltepunk- tes an. Ruecksprung zur Kommandoeingabe, ein ev. gewaehlter Haltepunkt wird verworfen. C change format Fuer einen der angezeigten L-Werte wird das Ausgabeformat geaendert. Es wird die vor der Variablen angegebene laufende Nummer erfragt, danach das Format im Sinne der TRON-Anweisung. Ausgabe der L-Werte im neuen Format, Ruecksprung zur Kommandoeingabe. E oder Sofortiger Abbruch des Programms. G go Das Programm laeuft ohne Halt weiter. H oder ? help Ein Hilfsmenue wird ausgegeben, in den alle Kommandos in Kurzform beschrieben sind. Das Menue ueberschreibt Teile des unteren Bild- schirmdrittels. Nach Druecken der Leertaste Ruecksprung zur Kommandoeingabe. S oder step Eine einzelne Anweisung wird ausgefuehrt, danach haelt das Programm wieder an. U until Setzen eines Wert-Haltepunktes: Das Programm haelt an, wenn eine Variable einen vorgegebenen Wert erreicht, ueber- oder unter- schreitet. Die Nummer der Variablen wird erfragt, dann ist der Wert im fuer Ausgabe der Variablen verwendeten Format einzugeben. Danach wird die Vergleichsoperation erfragt: Variable>, = oder < als vorgege- bener Wert. V value input Der Wert der soeben ausgefuehrten Anweisung kann geaendert werden. Die Eingabe erfolgt immer im Format %d. Dieses Kommando wird benutzt, um Testbedingungen in if-,while- und for-Bedingungen zu aendern. W write l-value Der Wert eines angezeigten L-Wertes wird geaendert. Die Eingabe er- folgt analog zum Kommando 'U', nur ohne Eingabe der Vergleichsopera- tion. 4.Hinweise zur Benutzung ------------------------ - Bei Benutzung des U-Kommandos arbeitet das Programm langsamer als beim B-Kommando. - Da der Wert eines Ausdrucks stets auf 'Integer' konvertiert wird, darf der Aufruf einer Funktion vom Typ 'void' nicht in einem Debug-Abschnitt liegen. - Der bei 'return' zurueckgegebene Wert kann nicht angezeigt werden. - Treten bei der Uebersetzung von trc.c Fehler auf, so sind die TRON/TROF- Kommentare genau zu ueberpruefen. Bei hartnaeckigen Fehlern (wo das modifizierte Quellprogramm richtig uebersetzt wird, aber nicht das transformierte) bitte den Autor verstaendigen! - Bei Timesharing-Systemen ist erst der Cursor unter der Zeile mit der Kommandoanforderung abzuwarten, bis ein Kommando gegeben wird! - Es sind 2KByte Speicher als Puffer fuer den Quelltext reserviert (Makro- definition fuer SRCBUF in trace.c). Sollte das Pufferende doch einmal erreicht werden, so erscheint die Fehlermeldung "wrong source file - end reached!". Abhilfe: vgl.7. 5. Zur Struktur von debug.c und trace.c ------------------------------------- debug.c: Auf der untersten Ebene erfolgt die zeichenweise Eingabe ueber bufrd(), eine eigene gepufferte Eingaberoutine. Das ist noetig, da oft Zeichen- gruppen mehrfach gelesen werden und ungetc() nicht genuegen wuerde. bufrd() verwendet den globalen Pufferzeiger bufptr. Auf der naechsten Ebene wird ein Zeichen mit rdchar() gelesen, wo u.a. Kommentare herausgeschnitten, Strings und Zeichenkostanten erkannt sowie Debug-Abschnitte markiert werden (ueber die globale Variable tron). rdchar() ruft trini(), das die TRON-Anweisung verarbeitet. Zuerst wird ein _trini()-Ruf geschrieben, dann (falls eine TRON-Liste folgt) _trad-Rufe. In transform() wird die eigentliche Transformation des Quellprogrammes vorgenommen. Von hier aus werden 3 wichtige Programme gerufen: key(), die spezielle Behandlung von Schluesselwoertern; testlab(), wo Marken unveraendert nach trc.c kopiert werden und trwrt(), das einen Ausdruck 'expr' in '_tr((int)(expr),offset)' umformt. Dabei ist 'offset' die Anzahl der seit Filean- fang gelesenen Bytes. trace.c: Vom Nutzerprogramm werden 3 Funktionen gerufen: _trini(): Hier wird Mini-Debug initialisiert (Quelltext in einen mit malloc() reservierten Speicherbereich laden, Halte- punkt nach der ersten Anweisung setzen u.a.). _trad(): Die Liste der in TRON angegebenen L-Werte wird bei jedem Ruf um 1 erweitert. In debug.c wurde bereits dafuer gesorgt, dass diese Liste max. 10 Elemente enthalten kann. Die Information ueber die L-Werte wird im globalen Feld v von Strukturen gespeichert. _tr(): Diese Funktion wird bei jeder Anweisung im Debug-Abschnitt gerufen. Hier wird geprueft, welcher Art der gesetzte Haltepunkt ist (entspr. U- oder B-Kommando) und ob er erreicht wurde. Wenn ja, werden - der Quelltext mit markierter aktueller Anweisung ausgege- ben (display()), - der Wert des Ausdrucks und die aktuellen L-Werte angezeigt (show()) und - getcmd() gerufen (Kommandoeingabe, von dort aus Ruf der anderen Funktionen). Danach wird das erste Argument (integer) von _tr() an das Nutzerprogramm zurueckgeliefert. 6. Uebertragung auf andere Rechner ---------------------------------- Debug.c und trace.c wurden unter RSX11M an VT 52 - kompatiblen Terminals mit dem Decus C Compiler entwickelt und auf ein UNIX-kompatibles System mit einem ADM 31 - Terminal uebertragen. Beide C-Programme beschraenken sich auf den bekannten, in Kernighan/Ritchie definierten Sprachumfang. Dadurch wird eine Uebertragung auf andere Rechner moeglich. Mittels der Macros am Beginn von trace.c und debug.c sind Betriebssystem und Terminaltyp einzustellen: In beiden Files muss der Name des aktuellen Betriebssystems durch den Wert 1 ersetzt werden, die Namen der anderen Systeme (z.Z. nur einer) durch 0. In trace.c ist analog der Terminaltyp zu definieren. Unter RSX ist das benutzte Terminal mittels SET auf VT52 einzustellen. Folgende C-Funktionen, die nicht ueblicherweise in C-Bibliotheken vorhanden sind, wurden benutzt: int kbin(): Eingabe eines Zeichens von der Tastatur aus ohne Echo auf dem Bildschirm. int kbinr(): Tastaturtest; max. 1 Sekunde Warten auf eine gedrueckte Taste. Wenn Taste gedrueckt: Rueckgabe des ASCII-Codes wie bei kbin(), sonst: return (-1). Diese Funktion wird beim A-Kommando benoetigt. Implementierungsabhaengig sind: rewind(): Der File wird stets nur gelesen (verwendet in trace.c/load()). Bei Schwierigkeiten muss der File geschlossen und neu eroeff- net werden, wie im vorliegenden Fall bei RSX. erasln(), erascr(), set_curs(): Funktionen zur Bildschirmansteuerung, in trace.c beschrieben. set_mark(): Diese Funktion gibt ein gut sichtbares Grafikzeichen zur Mar- kierung der aktuellen Anweisung aus. - In trace.c/show() wird vorausgesetzt, dass Tabulatoren alle 8 Zeichen ge- setzt sind. '\t' kann durch ' ' auf Kosten der Uebersichtlichkeit ersetzt werden. - In trace.c wird ein Bildschirm von 24*80 Zeichen angenommen. Die Breite ist mit #define SCRWDTH festgelegt (etwas kleineren Wert waehlen!). Von den Zeilen werden max. 21 genutzt. Fuer kleinere Bildschirme muessen ev. LINES und MAXSHOW veraendert werden (vgl.7.). - Bei jedem Halt wird ein Teil des Bildschirms neu geschrieben. Ausgabege- schwindigkeiten von 9600 Baud oder hoeher sind daher empfehlenswert. 7. Moegliche Aenderungen ------------------------ - Die Groesse des Quelltextfensters wird durch #define LINES 7 in trace.c festgelegt, die max. Anzahl gleichzeitig angezeigter L-Werte durch #define MAXSHOW 10 in trace.c und debug.c. Diese Werte koennen veraen- dert werden, wobei durch groessere 'LINES' bei jedem Halt mehr geschrieben wird. - Es werden mind. 2KByte Speicher (mit malloc()) fuer den aktuellen Quell- textabschnitt reserviert. Dieser Wert wird mit #define SRCBUF in trace.c vereinbart und kann veaendert werden. Der Weg ueber malloc() ist nicht unbedingt notwendig, es kann auch ein entspr. Feld vereinbart werden. - Das Quelltextfenster kann rollen, das Fenster fuer Ein-/Ausgabe des Nut- zerprogramms nicht. Diese unbefriedigende Situation ist fuer VT52-Termi- nals schwer zu aendern. Erforderlich waeren in trace.c: - Bei Eintritt in getcmd() muesste die aktuelle Cursorposition ermittelt und nach Austritt aus der 'for(;;)'-Schleife wieder gesetzt werden; - der Rollbereich muesste auf das untere Fenster beschraenkt bleiben; - die help-Funktion muesste anders formatiert werden, damit sie nicht das untere Drittel ueberschreibt. - Format und Typ des Wertes des angezeigten Ausdrucks liegen fest. Statt '%d' kann auch ein anderes Format gewaehlt werden. Dazu muss man in trace.c: - in show() statt 'printf("***value:\t%d...' das gewuenschte Format einsetzen und - in valinp(), else-Block, 'scanf("%d"...' aendern. Als Formate sind nur %d,%x,%o und %c moeglich. Es ist auch denkbar, dieses Format zur Laufzeit zu veraendern. Der Typ muss aber immer int oder char sein. - Beim B-Kommando koennte der Cursor auch durch die Steuertasten 'hoch' und 'runter' bewegt werden (anschliessend Suche des naechsten ';' oder ')'). Dazu ist die switch-Anweisung in trace.c/move_bp zu erweitern. - Auf den Inhalt von Pointern bzw. zu Feldelementen kann noch nicht zuge- griffen werden. Hierfuer muessten spezielle Formate oder Namen sowie Kommandos vereinbart werden.