Jeder Programmierer weiß es: normalerweise wird die meiste Zeit durch Fehlersuche in Anspruch genommen. Oft können dabei konventionelle Tools wie residente Debugger (Templemon!), Systemmonitore (SysMon!) oder Quelltext-Debugger und Profiler eingesetzt werden. Häufig kommt man so allerdings nicht weiter: sei es, weil der Fehler auf dem eigenen Rechner nie auftritt, der Fehler vom Betriebssystem selbst verursacht wird oder ein anderes Programm im Speicher das Problem verursacht.
Im folgenden möchte ich einige häufig auftretende Probleme vorstellen und dabei jeweils darauf eingehen, wie man sie in den Griff bekommen kann. Jedes der Verfahren funktioniert zur Laufzeit und benötigt keine spezielle Unterstützung durch andere Debugger. Dadurch können sie auch in ausgelieferten Produkten oder Beta-Versionen auf Rechnern anderer Leute eingesetzt werden.
Wer meint, seinen Betatestern vertrauen zu können (in letzter Zeit hört man sehr unschöne Geschichten über schwere Vertrauensbrüche), sollte Programme immer mit Symboltabelle weitergeben. Somit kann bei einem Absturz oftmals wenigstens der Name der betreffenden Funktion identifiziert werden. Allerdings gibt es tatsächlich viele Programmierer, die sich als professionell betrachten und nicht einmal dazu in der Lage sind, ein Programm in ATARIs Standard-Debugger db zu laden und einen Breakpoint zu setzen. Bei aller Liebe zu grafischen Oberflächen: so etwas darf einen Software-Entwickler eigentlich vor kein Problem stellen. Nun aber zu den angekündigten Tips:
Umfangreiche GEM-Programme enthalten meist eine große Menge von Dialogboxen. Jeder (oder fast jeder) dieser Dialoge enthält normalerweise Abbruch- und ‘ OK’ -Buttons, und damit sind die Gemeinsamkeiten zwischen verschiedenen Dialogen meist noch nicht erschöpft. Selbst bei strenger Disziplin bei der Namens vergäbe kann es da leicht passieren, daß man in einer Routine zwar auf den richtigen Objektbaum, aber den falschen Objektindex zugreift. Die daraus entstehenden Probleme sind oft fatal, in der Regel aber zumindest obskur und schwer zu finden.
Die Lösung: jeder Teil des Programms sollte nur die symbolischen Konstanten importieren, die ihn auch wirklich betreffen. Das Programm rscsplit zerlegt die vom Resource Construction Set angelegte Header-Datei in einzelne Bestandteile - für jeden Objektbaum einen. Dabei wird auch gleich noch festgestellt, ob sich die Daten eines jeden Baums überhaupt geändert haben. Damit werden überflüssige Neuübersetzungen von unbeteiligten Programmteilen verhindert.
Leider verfügt das AES-Resource-Konzept über keine Versionsüberprüfung. Veraltete oder beschädigte RSC-Dateien führen daher normalerweise zum sofortigen Absturz beim Programmstart. Selbst dann, wenn man dem Problem schon einmal begegnet ist, denkt man in der Regel zu allerletzt daran, die Resource-Datei zu überprüfen.
Eine häufige Quelle solcher Probleme ist übrigens, daß ein Archivierungsprogramm wegen falsch gesetzter Datumsinformationen eine neuere Resource-Datei nicht ausgepackt hat.
Abhilfe schafft eine selbst erzeugte Versionsinformation: rsccrc legt eine CRC-Prüfsumme über die Resource-Struktur an. Dabei werden die Inhalte von Buttons, deren Positionen u. ä. absichtlich nicht berücksichtigt, damit auch verschiedensprachliche Versionen eine gleiche Kennung erhalten. Der so berechnete Schlüssel wird sowohl in einer Include-Datei als auch in der Resource-Datei selbst vermerkt.
So kann zur Laufzeit unmittelbar nach rsrc_load() sichergestellt werden, daß die Datei tatsächlich in ordnungsgemäßem Zustand ist und im Zweifelsfall eine Fehlermeldung ausgegeben werden.
Beide Programme sind im Rahmen der Tool-Sammlung progtl01.zoo aus der Maus Münster 2 (0251/77...) zu beziehen.
Daß ein zerstörter Programmtext sofort zu Abstürzen führen kann, ist klar. Selbst unter MultiTOS mit eingeschaltetem Speicherschutz ist man davor nicht sicher: schließlich kann ja das eigene Programm der Übeltäter sein.
Vertrauen ist gut, Kontrolle ist besser! Daher kann es bei besonders kritischen Programmen nützlich sein, die Integrität des Textsegments regelmäßig zu überprüfen. Die in Listing 1 abgebildete Routine wird beispielsweise in SCSI-Tool benutzt, um vor unliebsamen Überraschungen durch Einflüsse anderer Programme sicher zu sein.
Hier sind grundsätzlich vier verschiedene Probleme zu unterscheiden.
Relativ harmlos sind Speicherlecks, also allozierte Speicherblöcke, die nicht freigegeben werden. Wenn das Programm nur kurz aktiv ist und dann terminiert wird, kann man sie getrost ignorieren. Schlimmer wirkt es sich da schon bei Programmen aus, die länger aktiv bleiben, wie zum Beispiel eine Shell. Grundsätzlich sollte man solchen Speicherlecks immer auf den Grund gehen, denn sie sind oft die Folge eines anderen Programmfehlers.
Malloc-Libraries tolerieren Speicherfreigaben mit falschen Speicherblockadressen zwangsläufig - schließlich ist die Funktion free()’ als void deklariert. Allein deshalb das Problem zu ignorieren, ist allerdings meist grob fahrlässig. Wenn man tatsächlich versucht, einen nicht allozierten Speicherblock freizugeben, sollte man dem unbedingt auf den Grund gehen.
Zugriffe auf bereits freigegebene Speicherblöcke sind meist schwer zu finden. Immerhin kann man sich aber bemühen, Zeiger auf freigegebene Speicherbereiche sofort auszunullen.
Ganz gefährlich sind natürlich Speicherschießer. Leider ist es nicht möglich, sie zuverlässig zu entdecken (es sei denn mit der Memory Changed-Funktion eines Debuggers). Immerhin kann man sich aber vor einem besonders häufigen Typ des Speicherschießers absichern. Oft kommt es nämlich vor, daß über das Ende eines allozierten Speicherblocks hinausgeschrieben wird.
Was tun? Eine spezielle Library zur Speicherverwaltung schafft Abhilfe. Sie greift ausschließlich auf die Standardfunktionen der C-Library zurück und sollte damit vom verwendeten Compiler unabhängig sein. Da die erweiterte Funktionalität sowohl Speicher als auch Rechenzeit frißt, kann per #define zwischen zwei verschiedene Versionen umgeschaltet werden. Dabei finden in der kleinen Fassung die meisten Checks nicht statt, dafür ist der Overhead aber auch nur sehr gering.
Es ist verlockend, eine solche Bibliothek per Makros transparent zu implementieren. In diesem Fall wurde allerdings darauf verzichtet, da die neuen Funktionen sowieso eine andere Parametrisierung aufweisen. Die abgedruckten Funktionen kommen seit einiger Zeit in SCSI-Tool zum Einsatz und haben sich dort schnell bewährt.
Die Listings 2 und 3 zeigen die Include-Datei und die Implementation. Je nachdem, ob MEMDEBUG gesetzt ist, wird dabei die große oder die kleine Lösung übersetzt. Definiert werden: allocatef), allocate_st(), release() und MEMIN1T.
allocate() und allocate_st() allozieren Speicherblöcke, wobei letztere nur solche aus dem ST-RAM anfordert. Im Gegensatz zu malloc() ist dabei garantiert, daß der Speicherblock nur Nullen enthält (eine weitere Vorsichtsmaßnahme!), release() verhält sich wie free(), nur daß ein Zeiger auf den Zeiger übergeben wird. Dadurch kann das Ausnullen des Zeigers automatisch erfolgen.
MEMINIT sollte direkt zu Programmbeginn benutzt werden und sorgt dafür, daß am Programmende überprüft wird, ob auch wirklich alle angeforderten Speicherblöcke freigegeben wurden.
Nun zur Implementation (siehe Listing 3): is_physical() wird dazu benutzt, um festzustellen, ob ein Speicherblock im ST-RAM liegt. Wer diese Funktionalität nicht braucht, kann natürlich einfach die entsprechenden Programmteile entfernen.
Die Struktur memblock wird in der großen Fassung für jeden allozierten Speicherblock angelegt. Sie enthält zur Absicherung eine Magic Number, einen Zeiger auf den Namen der anfordernden Quelltextdatei [von allocate() per Makro automatisch mit übergeben], die Zeilennummer (ebenso), die Größe des Blocks, einen Zeiger zur Verkettung sowie ein Flag, anhand dessen erkannt wird, ob der Speicherblock per Library oder per GEMDOS angefordert worden ist. Die Library legt diese Struktur vor jeden allozierten Speicherblock und verkettet alle Strukturen zu einer einfachen Liste. Unmittelbar vor und hinter dem eigentlichen Speicherblock wird eine weitere Kennung - der Guard - angelegt.
memory_allocate addiert zunächst den Platz für Verwaltungsinformation zur angeforderten Länge. Zuerst wird der Block per malloc() angefordert. Wenn ST-RAM angefordert worden war und der Block nicht im ST-RAM liegt, wird statt dessen Mxalloc() probiert.
Schließlich werden die verfügbaren Informationen in die Struktur eingetragen, der Speicherblock gelöscht, die Verkettung hergestellt und der Speicherblock zurückgegeben.
memory_release gibt einen Speicherblockfrei. Dabei können verschiedene Fehler aufgedeckt und gemeldet werden. So wird gleich zu Beginn getestet, ob die Verkettung der Infostrukturen noch in Ordnung ist („Memory block at xyz destroyed.“)- Anschließend werden die Magic Numbers sowie die Guards überprüft [„Memory block at xyz (from file a, line b) destroyed“].
An dieser Stelle wird schon klar, wozu beim Allozieren der Dateiname und die Zeilennummer vermerkt wurden: auf diese Weise kann man leicht erkennen, welcher Speicherblock betroffen ist.
Wenn der freizugebende Block erreicht ist, wird er freigegeben. Wer mag, kann an dieser Stelle auch noch den freigegebenen Speicherblock löschen: die abgedruckten Listings sind wie gesagt nur ein Gerüst, das man nach seinen persönlichen Bedürfnissen modifizieren kann. Wurde der Block hingegen nicht gefunden, gibt es eine Fehlermeldung („Freeing unallocated memory at xyz, called from...“). Bleibt memory_exit, das direkt bei Programmende (durch ate-xit) aufgerufen wird. Diese Funktion gibt Informationen über alle noch nicht freigegebenen Speicherblöcke aus („Unfreed memory at...“).
Soviel zu diesem Thema. Aus Sunnyvale gibt es - abgesehen von ersten Meldungen von Jaguar-Verkaufserfolgen - kaum Neuigkeiten. Immerhin hat man sich aber nach einigem Hin und Her dazu durchringen können, die seit Monaten fertige neue SpeedoGDOS-Version freizugeben. Wichtigster Vorteil: Speedo-Fonts mit Postscript-Character-Encoding (im PC-Markt relativ häufig) sowie Speedo-Fonts mit Demo-Kodierung (das sind die acht mit X11R5 ausgelieferten freien Schriftschnitte) können benutzt werden. Immerhin: wenn schon nichts Neues entwickelt wird, so finden wenigstens allmählich die zuletzt entwickelten Versionen ihren Weg in die Hände der Kunden.
/* Checksumme über Textsegment berechnen.
Muß einmal mit init == TRUE initialisiert
werden. Liefert FALSE bei Fehler */
/* _BasPag ist Pure-C-spezifisch! */
static boolean
check_text_seg (boolean init)
{
static unsigned int oldsum;
unsigned int sum = 0;
unsigned int *p = _BasPag->p_tbase;
long i;
long length_w = _BasPag->p_tlen / sizeof (unsigned int);
for (i = 0; i < length_w; i++)
sum += *p++;
if (init)
oldsum = sum;
else
if (oldsum != sum)
return FALSE;
return TRUE;
}
/*
memory.h
Atarium Februar 1994
(c)1994 by MAXON-Computer
Autor: Julian F. Reschke
16. Dezember 1993
*/
#ifdef MEMDEBUG
void *memory_allocate (size t, const char *, long, int);
void memory_release (void **, const char *, long);
void memory_exit (void);
#define allocate(a) memory_allocate(a, __FILE__,__LINE__,0)
#define release(a) memory_release((void **)a,__FILE__,__LINE__)
#define allocate_st(a) memory_allocate(a, __FILE__,__LINE__,1)
#define MEMINIT atexit(memory_exit)
#else
void *memory_allocate (size_t, int);
void memory_release (void **);
#define allocate(a) memory_allocate(a,0)
#define allocate_st(a) memory_allocate(a,1)
#define release(a) memory_release((void **)a)
#define MEMINIT
#endif
#define realloc ARG! /* nicht implementiert */
/*
memory.c
Atarium Februar 1994
(c)1994 by MAXON-Computer
Autor: Julian F. Reschke
16. Dezember 1993
*/
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tos.h>
#include "memory.h"
#define MEMMAGIC1 'JURE'
#define MEMMAGIC2 "MEMGUARD"
#define GUARDLEN 8
#define phystop ((unsigned long *) 0x42eL)
static int
is_physical (void *p)
{
static unsigned long pt = 0L;
if (! pt) {
long oldsp = Super (0L);
pt = *phystop;
Super ((void *) oldsp);
}
return ((unsigned long) p) < pt;
}
#ifdef MEMDEBUG
typedef struct memblock
{
long magic;
const char *file;
long line;
size_t amount;
struct memblock *next;
int is_Malloced;
char bla[1];
};
static struct memblock *memlist = NULL;
void *
memory_allocate (size_t amount, const char *file, long line, int flag)
{
size_t a = amount + sizeof (struct memblock) + 2 * GUARDLEN;
struct memblock *m = malloc (a);
boolean malloced = FALSE;
if (flag)
{
/* Wenn der Alloc funktioniert hat, aber FastRAM herausgekommen ist */
if (m && !is_physical(m)) {
free (m);
m = NULL;
}
/* Mxalloc probieren */
if (!m) {
long ret;
ret = (long) Mxalloc (a, 0);
if (ret == -32L) return NULL;
malloced = TRUE;
m = (struct memblock *)ret;
}
}
if (!m) return NULL;
m->is_Malloced = malloced;
m->magic = MEMMAGIC1;
m->file = file; m->line = line;
m->next = memlist;
m->amount = amount;
memlist = m;
strncpy (m->bla, MEMMAGIC2, GUARDLEN);
strncpy (m->bla + GUARDLEN + amount, MEMMAGIC 2, GUARDLEN);
memset (m->bla + GUARDLEN, 0, amount);
return m->bla + GUARDLEN;
}
void
memory_release (void **p, const char *file, long line)
{
struct memblock *last = NULL;
struct memblock *l;
for (l = memlist; l; l = l->next)
{
if (l->magic != MEMMAGIC1)
{
printf ("Memory block at %p"
" destroyed.\n", l);
getchar ();
exit (0);
}
if (strncmp (l->bla, MEMMAGIC2, GUARDLEN) || strncmp (&l->bla[GUARDLEN + l->amount], MEMMAGIC2, GUARDLEN))
{
printf ("Memory block at %p (from file %a," line %1d) destroyed.\n",
l, l->file, l->line);
getchar ();
exit (0);
}
if (&l->bla[GUARDLEN] == *p)
{
*p = NULL;
if (!last)
memlist = l->next;
else
last->next = l->next;
if (l->is_Malloced)
Mfree (1);
else
free (1);
return;
}
last = 1;
}
printf (”\nFreeing unallocated memory at %p."
" called from file\n%s, line %1d. "
"The program will be terminated,\n"
"press any key to continue\n",
*p, file, line);
getchar ();
exit (0 ) ;
}
void
memory_exit (void)
{
struct memblock *l;
int found = 0;
for (l = memlist; l; l = l->next)
{
found = 1;
printf ("\nUnfreed memory at %p, %ld bytes,"
" containing %081x %081x,\n"
"allocated from %s in line %1d.\n",
l, l->amount,
*((long *)&l->bla[GUARDLEN]),
*((long *)&l->bla[GUARDLEN + sizeof (long)]),
l->file, l->line);
}
if (found) {
puts ("Press any key to continue.");
getchar ();
}
}
#else
void *
memory_allocate (size_t size, int flag)
{
int *m = malloc (size + sizeof (int));
boolean malloced = FALSE;
if (flag)
{
/* Wenn der Alloc funktioniert hat, aber FastRAM herausgekomraen ist */
if (m && !is_physical (m)) {
free (m);
m = NULL;
}
/* Mxalloc probieren */
if (!m) {
long ret;
ret = (long) Mxalloc (size + sizeof(int), 0);
if (ret == -32L) return NULL;
malloced = TRUE;
m = (int *)ret;
}
}
if (!m) return NULL;
*m = malloced;
memset (m + l, 0, size);
return m + 1;
}
void
memory_release (void **a)
{
int *p = (int *)a;
if (*p)
free (p);
else
Mfree (p);
}
#endif