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

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 */
	}
}


Aus: ST-Computer 04 / 1988, Seite 132

Links

Copyright-Bestimmungen: siehe Über diese Seite