← ST-Computer 04 / 1988

BUG ALERT: Ein Mini-Tool zum Entwanzen von C-Programmen unter GEM

Programmierpraxis

Die Sprache C lĂ€ĂŸt dem Programmierer bei der Arbeit viel Freiheit - leider auch die Freiheit, Fehler zu machen. Ein großer Anteil der Programmierarbeit besteht so in der Fehlersuche. Arbeitet man mit einem Compiler, der ĂŒber die normale Syntax-PrĂŒfung hinaus wenig UnterstĂŒtzung bei der Fehlersuche bietet, ist man darauf angewiesen, sich mit Hilfe von ‘printf’- und ‘scanf’- Aufrufen, mĂŒhselig an den Fehler heranzutasten.

Sehr problematisch wird diese Methode aber, wenn man unter GEM programmiert. Die eingestreuten printf-Befehle bringen den ‘Schreibtisch’ heillos durcheinander, und man tappt dann mit der Maus auf einem weißen Monitor herum, um die Programmknöpfe zu finden!

Nachdem mir diese Methode mehrfach die ganze MenĂŒleiste zerstört hatte, ĂŒberlegte ich mir, daß es wohl sinnvoller sei, den Problemen eines GEM-Programms mit den Mitteln zu begegnen, die GEM zur VerfĂŒgung stellt, nĂ€mlich den Dialogboxen. Diese haben ja den Sinn, dem Programmbenutzer Informationen ĂŒber den augenblicklichen Zustand des Programms zu ĂŒbermitteln (Helptexte, Fehlermeldungen). Anfangs verstreute ich die Alertboxen in der NĂ€he des vermuteten Fehlers direkt im Programm und entfernte sie nach dem Auffinden des Fehlers wieder. Als ich aber dann in einem besonders vertrackten Modul die Alertboxen mehrfach eingesetzt und wieder abgerĂ€umt hatte, kam mir die Idee, sie doch zunĂ€chst einmal im Programm zu lassen und ĂŒber eine globale Boolsche Variable ‘tr’ einen Trace - Modus zu simulieren.

Im kritischen Programmbereich haben die Funktionen dann folgendes Aussehen:

x_funktion() { if(tr) gprintf("x_funktion"); ... ... ... }

oder:

x_funktion(arg1, arg2) int arg1; float arg2; { if (tr) gprintf("x_funktion|arg1 = %darg2 = %2f", arg1, arg2) ... ... ... }

Die Funktion meldet sich dann bei eingeschaltetem Trace-Modus mit:

Alertbox 1

Bei diesem Vorgehen ist nach dem ersten Durchlauf klar, in welcher Funktion der Wurm steckt. Eine genaue ÜberprĂŒfung dieser Funktion nach folgender Checkliste fĂŒhrt oft schon an dieser Stelle zum Finden des Fehlers:

  1. Werden die GEM-Funktionen mit der richtigen Zahl von Parametern aufgerufen ??
  2. Stimmen die Typen der ĂŒbergebenen Parameter mit den von der Funktion erwarteten ĂŒberein ??
  3. Semikolon hinter ‘for’, ’while’ oder ‘if - Klammer ??
  4. Zuweisungsoperator ‘=’ wo eigentlich ‘==’ stehen sollte ?
  5. Hat sich ein Zeichen (z.B. ‘;’) ganz weit an den rechten Rand des Listings verirrt, so daß es beim normalen Durchscrollen des Quelltextes nicht zu erkennen ist??

Falls die ÜberprĂŒfung dieser fĂŒnf Punkte keinen Erfolg bringt, sollte man sich als nĂ€chstes die folgenden Fragen vorlegen:

  1. Sind alle Pointer richtig initialisiert ?
  2. Sind alle Datenstrukturen richtig initialisiert ?

Zur Beantwortung dieser Fragen bieten sich nun die Funktionen an, um die es in diesem Artikel geht: ‘gprintf und ‘dump’:

Mit gprintf kann man sich den jeweils aktuellen Wert der Variablen in jedem Format ausdrucken lassen, das vom normalen ‘printf-Befehl erzeugt werden kann - dump erzeugt einen Hex-Dump von 4 mal 8 Bytes ab der angegebenen Speicheradresse. Den Aufruf dieser Funktionen macht man nun besser nicht von ‘tr’ abhĂ€ngig, denn da man zu diesem Zeitpunkt die Lage des Fehlers kennt, wĂ€re es sinnlose Zeitvergeudung, sich im Trace-Modus bis zur fraglichen Funktion durchzuklicken!

Angenommen ‘gprintf’ oder ‘dump’ zeigen fĂŒr einen Pointer den Wert Null an. Nun ist es natĂŒrlich sinnvoll, das Programm an dieser Stelle zu unterbrechen - denn ein Nullpointer in Aktion macht den Griff zum Resetknopf unausweichlich. - Und wenn auch die benutzte Ramdisk resetfest ist, der Zeitverlust gegenĂŒber einem geordneten Programmabbruch ist doch erheblich: Abfragedialog, ob Ramdisk so bleiben soll. Wiederaufbau des Desktops, Anzeige der Directories, Neustarten der Shell., also es st schon sinnvoll zu programmieren:

x_funktion() int var; ... ... if (!gprintf("var = %d",var)) return; ...

Falls ein Fehler behoben ist, kann man sich dann durch Anklicken von 'TRON’ zum nĂ€chsten Fehler durchklicken!!

Beschreibung der Funktionen

/***********************/ /* gprintf() */ /***********************/

Die Funktion ‘gprintf’ entspricht veitgehend der normalen ‘printf’-Funktion. Nur daß die Ausgabe nicht auf dem vollen Bildschirm, sondern in kleinen Alertboxen mit je fĂŒnf Zeilen zu dreißig ASCII-Zeichen erfolgt. Es ist daher klar, daß Steuerzeichen im Formatstring und in Ausgabestrings nur mit Vorsicht zu verwenden sind.

Ist im Formatstring kein Zeilensprungzeichen ‘\n’ vorhanden, ĂŒbernimmt gprintf automatisch die Zeilenverteilung und zwar so, daß nach jeder Ausgabe eines Variablenwerts eine neue Zeile angefangen wird. - Ist der Ausgabestring einer Variablen lĂ€nger als 30 Zeichen, wird nach dem 30. Zeichen automatisch eine neue Zeile angefangen - andernfalls wĂŒrde die Funktion form_alert Bomben erzeugen.

Hat man aber im Formatstring ein einziges ‘\n’ - Zeichen verwendet, ist man fĂŒr die Verteilung auf die Zeilen selbst verantwortlich!! gprintf sorgt jetzt nur noch fĂŒr den Zeilenwechsel nach dem 30. Zeichen. - Diese Betriebsweise von gprintf ermöglicht es zum Beispiel, mehr als eine Variable in eine Zeile zu packen.

Durch Verwendung des Alertbox-Zeilentrenners ‘|’ kann man zu den Zeilentrennern des Automatik-Betriebs weitere zusĂ€tzliche Zeilentrenner einfĂŒgen.

Insgesamt darf ein gprintf-Befehl 960 Zeichen ausgeben - natĂŒrlich verteilt auf mehrere aufeinanderfolgende Alertboxen.

Falls der ABBRUCH - Button angeklickt wurde, gibt geprintf den Wert 0 an die aufrufende Funktion zurĂŒck -andernfalls den Wert 2.

/*************************/ /* dump() */ /*************************/

Die dump-Funktion verlangt als Argument eine Speicheradresse (also) long - Format. Sie erzeugt ein Hex-Dump von vier Zeilen zu jeweils 8 Byte ab der angegebenen Speicherstelle. In einer fĂŒnften Zeile werden 30 der 32 Bytes als Zeichenkette ausgegeben. Die letzten beiden Zeichen passen leider nicht mehr in die Box! Zeichen mit Ziffemcodes unter 31 und oberhalb 166 werden als Punkte ausgegeben.

Durch Anklicken des WEITER-Buttons können jeweils die nÀchsthöheren 32 Bytes des Speichers ausgegeben werden.

Variable Funktionsparameter

Der Quelltext ist ausfĂŒhrlich kommentiert und selbsterklĂ€rend. Eine Wiederholung der Kommentare an dieser Stelle wĂŒrde keine zusĂ€tzliche Information bringen. - Lediglich die ArgumentĂŒbergabe an gprintf muß nĂ€her erklĂ€rt werden. Da die Funktion gprintf Argumente unterschiedlichen Typs und in unterschiedlicher Anzahl verarbeiten muß, kann die normale ArgumentĂŒbergabe hier nicht verwendet werden, gprintf macht sich bei der ArgumentĂŒbemahme das Verfahren zunutze, mit dem die aufrufende Funktion die Argumente generell ĂŒbergibt - nĂ€mlich ĂŒber den “Stack”. Mit Hilfe der dump-Funktion lĂ€ĂŸt sich dieses Verfahren leicht demon-stieren:

Nehmen wir an, gprintf wird an irgendeiner Stelle des Programms mit den folgenden Argumenten aufgerufen:

int i = 0x1234; long j = 0x22222222; static char k[] = "Dies ist ein String" gprintf("Var. i = %xVar. j = 1%xVar. k = %s",i,j,k);

Das Programm legt nun diese Argumente von rechts beginnend nacheinander auf den Stack: erst die vier Bytes der String-Adresse k, dann die vier Bytes der long-Variablen j , die beiden Bytes von i (int!) und dann die vier Adreßbytes der form_str - Variablen. Zuletzt wird die RĂŒckkehradresse auf den Stack gelegt!! Dabei darf man nicht vergessen, daß der Stack von hohen zu niedrigen Speicheradressen - also nach unten wĂ€chst!!

Alertbox 2

gprintf kennt nun die Anfangsadresse dieses Stacks ĂŒber die Adresse des ersten Arguments. In der Alertbox 2 ist das Hexdump dargestellt, das sich beim Aufruf von dump(&form_str) ergibt.

Die ersten vier Bytes enthalten die long - Zahl 9C276, nĂ€mlich die Adresse eben dieser Variablen ‘form_str’, danach folgen die Variablen i(1234)und k(22222222)sowie die Adresse des Strings k. (0009C1FC). Alertbox 3 geht der Adresse 9c276 nach und druckt prompt den Formatstring aus:

Alertbox 3

Um diese Variablen aber in der richtigen Weise ĂŒbernehmen zu können, verfĂ€hrt gprintf nun folgendermaßen: ZunĂ€chst einmal wird der Zeichenpointer ‘char *pargu’ auf die Anfangsadresse der zweiten Variablen ‘argu’ gesetzt. Durch die Analyse des Formatstrings erfĂ€hrt die Funktion, daß es sich bei diesem Argument um den Typ ‘int’ handelt und ĂŒbergibt diesen Sachverhalt an das im Programmkopf definierte Funktions-Macro ‘arg_wert’. Dieses erhöht nun den Wert des Pointers pargu um die Bytezahl einer int-Zahl: pargu += sizeof(int) (Da es sich um einen char-pointer handelt, funktioniert die Addition hier normal wie bei einer long-Zahl). Nachdem der Zeiger nun genau hinter die int-Variable i positioniert ist, wird er durch den Cast (int *) zu einem Zeiger auf ein array von int-Zahlen gemacht und mit (...) [-1] das hinter dem pointer liegende Element angesprochen.

Das gleiche Verfahren wird nun nacheinander mit den ĂŒbrigen Argumenten durchgefĂŒhrt. Man sieht aber, daß gprintf und natĂŒrlich auch printf selbst ganz schön durcheinander kommt, wenn die Zahl oder der Typ der im Formatstring angegebenen Argumente nicht mit der Zahl oder dem Typ der tatsĂ€chlich ĂŒbergebenen Argumente ĂŒbereinstimmt. In diesem Fall hat die Übergabe offensichtlich geklappt, denn gprintf gibt die folgende Alertbox aus:

Einbindung der Funktionen in eigene Programme.

Die Verwendung der Funktionen in eigenen Programmen sollte keine Probleme aufwerfen. Lediglich ‘tr’ muß als globale Variable im Programm deklariert werden und bei den Programmdefinitionen mĂŒssen die Funktionen: arg_wert(pargu, type) und Super(stack) enthalten sein. Ansonsten wird der Funktionsblock irgendwo ins Programm kopiert. Vielleicht kann er sich ja beim nĂ€chsten Bombenalarm schon nĂŒtzlich machen.

Dr. Peter HĂ€rtel

/***************************************************/ /***************************************************/ /* */ /* BUGALERT. C */ /* (Mini-Debugging-Tool fĂŒr C-Programme unter GEM) */ /***************************************************/ /***************************************************/ /*----------------*/ /* Programm - Definitionen */ /*__________________________*/ #define arg_wert(pargu,type) ((type *)(pargu += sizeof(type)))[-1] /* Argument fangen */ #define Super(stack) gemdos(0x20,stack) /* Supervisormodus */ /*----------------*/ /* Globale Variable */ /*__________________*/ int tr; /* Trace-Schalter */ /*----------------*/ /* gprintf */ /*----------------*/ gprintf(form_str, argu) /* Ersatz fĂŒr printf in GEM-Pro- */ char *form_str; /* grammen. Formatstring wie bei */ /* 'printf'. */ { char *pargu = (char *) &argu; /* C-Zeiger auf Argumente-Stack */ char atext[200]; /* Puffer fĂŒr Alerttext */ char text[1000]; /* Puffer fĂŒr Textausgabe */ char *s; /* Zeiger auf diesen Puffer */ char f_str[13]; /* Formatanweisung 12 Zeichen */ char *p; /* Zeiger auf Formatanweisung */ char auswahl[22]; /* String fĂŒr Auswahl-Buttons */ int cz = 0, /* Anzahl Zeichen von sprintf */ zz, /* Anzahl Zeilen im Text */ vt, /* Argumenttyp 1=int, 2=long.. */ wahl, /* Returnwert von form_alert */ autom, /* automatische Zeilentrennung */ laenge; /* LĂ€nge des Ausgabestrings */ s = &text[0]; /* Zeiger auf Puffer setzen */ strcpy(atext,"[0]["); /* Anfang Alertbox-strings */ strcpy(auswahl,"][ABBRUCH|TRON | OK ]"); if(tr) /* falls tr = wahr,dann TRON - */ { /* in TROFF verwandeln */ auswahl[13]='F'; auswahl[14]='F'; } p = form_str; while((*p++) && (*p != '\n')) /* PrĂŒfen, ob '\n' vorhanden */ ; autom = (*-p) ? 0 : 1; /* falls vorhanden, autom=FALSE */ /*-------------------------*/ /*—Formatstring in einen Textstring von max.--*/ /*-------950 Zeichen umwandeln-------*/ for(laenge = 0;*form_str;) { if(*form_str != '%') /* solange keine Formatanweisung */ { laenge++; /* Zeichen zĂ€hlen */ *s++ = *form_str++; /* Text in Textpuffer schreiben */ } else /* Variablen - Formatanweisung */ { strncpy(f_str,form_str,12); /* 12 Zeichen Formatanweisung */ p = f_str; /* wieviel davon sind gĂŒltig? */ while((*++p<='9' && *p>='0') || *p == '.') /* Feldweite in Ziffern & Punkt */ ; /* falls vorhanden Zeiger++ */ switch(*p) /* auf gĂŒltiges Zeichen prĂŒfen */ { case 'h': /* h heißt short-Variable */ p++; /* nĂ€chstes Zeichen ist wichtig */ case 'c': case 'd': case 'x': case 'o': case 'u': vt =1; /* Variablentyp int */ break; case '1': vt =2; /* 1 heißt long-Variable */ p++; /* nĂ€chstes Zeichen ist wichtig */ break; case 's': vt =3; /* String-Pointer ist gemeint */ break; case 'f': case 'e': case 'g': vt =4; /* Variable vom Typ double */ break; case '%': /* Nur Prozentzeichen schreiben */ vt =5; break; default: /* alle and. Zeichen -> error */ form_alert(1,"[3] [Formatanweisung nicht korrekt][ABBRUCH]"); return(1); } *++p='\0'; /* Stringende Variablenformat */ form_str += strlen(f_str); /* Pointer auf nĂ€chstes Zeichen */ switch(vt) /* Je nach Variablentyp die rich- */ { /* tige Variable vom Stack holen */ case 1: cz=sprintf(s,f_str, arg_wert(pargu,int)); /* int */ break; case 2: cz=sprintf(s,f_str, arg_wert(pargu, long)); /* long */ break; case 3: cz=sprintf(s,f_str,arg_wert(pargu,char *)); /* pointer */ break; case 4: cz=sprintf(s,f_str, arg_wert(pargu,double)); /* double */ break; case 5: if(*-s == '|') { *s++ = '%'; *s++ = '|'; continue; } else { *++s='%'; ++s; continue; } } laenge += cz+1; if(laenge > 960) *form_str ='\0’; /* aufhören bevor Puffer voll ist */ s += cz; if(autom && !(vt==5)) /* falls automatische Zeilentrennung */ *s++ = '|'; /* neue Zeile fĂŒr neue Variable! */ } /* Ende else */ } /* Ende while */ if(cz && autom) /* letztes '|'-Zeichen löschen! */ s--; *s = '\0'; /* String-ende markieren! */ /*------Textstring fĂŒr die Ausgabe-----*/ /*--in mehreren Alert-Boxen vorbereiten.------*/ /* cz=ZeichenzĂ€hler zz=ZeilenzĂ€hler */ /* Zeiger auf Stringanfang setzen */ /* und bis zum Ende des Textstrings Vorarbeiten */ for(cz=1,zz=0,s=&text[0],p=&atext[4]; *s ;cz++,p++,s++) { if((*s<31) || (*s>166) /* handelt es sich um Textzeichen? */ || (*s==91) || (*s==93)) /* [ und ] sind nicht erlaubt */ *p='?'; else *p = *s; /* Zeichen in Ausgabetext kopieren */ if((*s == '|') /* Zeilentrenner gefunden */ || (*s == '\n') || (cz ==30)) /* Zeilenende ohne Trennung erreicht */ { if(cz == 30) p++; /* Zeiger auf nĂ€chstes Zeichen */ while(cz++<30) /* Rest der Zeile mit space fĂŒllen */ *p++=' '; *p = '|'; /* Zeilentrenner in Ausgabetext */ cz=0; /* ZeichenzĂ€hler zurĂŒcksetzen */ if(++zz == 5) /* ZeilenzĂ€hler erhöhen */ { /* falls vierte Zeile, */ *p='\0'; /* dann Ausgabeende */ strcat(atext,auswahl); /* Ende form_alert-string anfĂŒgen */ wahl=form_alert(3,atext); /* Alert-box ausgeben */ if(wahl==2) /* TRON-TROFF Button wurde gewĂ€hlt */ tr = !tr; if(wahl==1) /* Abbruchbutton wurde gewĂ€hlt! */ return(0); p = &atext[4]; cz = 1; zz = 0; /* ZeilenzĂ€hler zurĂŒcksetzen */ } } } /* Ende for - Schleife */ if(!zz) /* falls keine vollstĂ€ndige Zeile */ while (cz++ < 31) /* Rest der Zeile mit space fĂŒllen */ *p++=' '; *p = '\0'; strcat(atext,auswahl); wahl = (form_alert(3,atext)); /* Alert-box ausgeben */ if(wahl==2) /* TRON-TROFF Button wurde gewĂ€hlt */ tr = !tr; /* Schalter umlegen */ return(wahl-1); /* falls ABBRUCH dann Null zurĂŒck */ } /*----------------*/ /* dump */ /*----------------*/ dump(adr) /* Gibt 32 Bytes ab RAM-Adresse */ char *adr; /* 'adr' als Hex-Zahlen aus */ { int i,j; /* SchleifenzĂ€hler */ unsigned int byte; /* auszugebendes Byte */ int h_n, /* higher Nibble */ l_n, /* lower Nibble */ wahl; /* Alert-Box-Button */ long hexad; /* Adresse als Zahl */ char c; /* Zeichenwert des Byte*/ char atext[200]; /* Ausgabestring */ char *s; /* Zeiger auf atext */ char *t; char *p; char *pt; long save_s; strcpy(atext,"[0]["); /* Anfang Alertbox */ if((long)adr<0x800) /* PrĂŒfen, ob Supervisor-Modus nötig */ if(form_alert(1,"[3][Die angegebene Adresse liegt| im geschĂŒtzten Speicherbereich|\Abbrechen ?][JA|NEIN]") == 1) return (0); while(1) /* bis ABBRUCH gewĂ€hlt */ { if((long)adr<0x800) /* falls nötig, Supervisormodus->ein */ save_s = Super(0L); p = adr; /* Zeiger auf RAM-Adresse */ s = &atext[4]; /* Zeiger auf Hex-Ausgabe */ pt = &atext[128]; /* Zeiger auf Textausgabe */ for (i=0;i<4;i++,s++) /* vier Zeilen ausgeben */ { t = s+6; /* Adresse rĂŒckwĂ€rts ausgegeben */ s += 7; *t—=' '; *t—=':'; hexad = (long)(adr+8*i); for(j=0;j<5;j++) /* Hexadresse 5-stellig ausgeben */ { byte = (int)hexad & 0xFL; /* nur die letzte Stelle interessiert*/ *t—=byte+((byte>9) ? 55:48); /* Hex-Zeichen ausgeben */ hexad /= 16; /* nĂ€chste Stelle ausgeben */ } for (j=0;j<8;j++) /* Eine Zeile mit 8 Byte ausgeben */ { byte = *p++; /* hole byte aus Speicher */ /* Nur Textzeichen ausgeben */ *pt++=((byte>31 && byte<91) || (byte>93 && byte<166)) ? byte : '.'; h_n = byte>>4; /* high-Nibble - h_n */ *s++ = h_n +((h_n > 9) ? 55:48); /* Dez/Hex-Umwandlung */ l_n = byte & 0xF; /* lower-Nibble = l_n */ *s++ = l_n +((l_n > 9) ? 55:48); *s++=' '; /* Leerzeichen nach Byte */ } *—s='|'; /* Zeilenende markieren */ } pt -= 2; /* Stringende markieren */ *pt='\01; if ((long)adr<0x800) /* Supervisormodus aus */ Super(save_s); strcat(atext,"] [ABBRUCH|WEITER| OK ]"); /* Buttontext anhĂ€ngen */ if((wahl - form_alert(3,atext)) != 2) /* Alertbox ausgeben */ return(wahl-1); */ /* falls Wiederholung (2) */ adr += 32; /* neue Adresse im RAM */ } }