B Speicherschutz und Speicherlöcher

 

Hier werden die Ideen vom Observer dargestellt. Dazu gehören die Makros, die C++-Methoden und das auswertende Perl-Skript (in Verbindung mit dem GNU-Debugger gdb).

Die dynamische Verwaltung von Speicher ist in C++ nicht so einfach zu handhaben, wie z.B. in Java, da hier kein automatisches Aufräumen stattfindet. Bei der Programmierung muß man daher darauf achten, daß jedes angeforderte Stück Speicher auch wieder freigegeben wird. Dies wird in größeren Quellen sehr schnell unübersichtlich.

Ebenfalls ist in C++ kein Mechanismus eingebaut, der bei Überschreiten von Arraygrenzen einen Fehler meldet. Ein falscher Zugriff führt entweder zu einem sofortigen Abbruch des Programms (segmentation fault) oder löst im weiteren Verlauf Fehler aus, die nicht nachvollziehbar sind.

Zur Lösung der beiden Probleme ist eine Klasse entstanden, die es möglich macht, Fehler in der Speicherverwaltung aufzudecken und damit einen großen Anteil an der Stabilität des Codes zu gewährleisten. Das Verfahren ist einfach und kann auch in fremde Programme aufgenommen werden.

In C++ sind nur zwei Methoden möglich, um dynamisch Speicher zu belegen und freizugeben. Für skalare Werte ist dies das Methodenpaar new / delete, bei Arrays kommen die eckigen Array-Klammern [] dazu. Diese vier Aufrufe sind durch vier Makros ersetzt worden, die statt new und delete in den Code gebaut werden:

     NEW( ptr, class, init )
     DELETE( ptr )
     NEW_ARRAY( ptr, class, size, init )
     DELETE_ARRAY( ptr, class, size )

Wobei ptr ein Zeiger auf das Objekt ist, class der Typ oder der Klassenname des Objekts, init eine Argumentliste für den Konstruktor (in runden Klammern) und size die Anzahl der Elemente im Array.

Weiterhin wird ein Makro debug_printf() zur Verfügung gestellt, das die gleichen Argumente besitzt wie der printf-Befehl, allerdings mit doppelter Klammerung.

Standard C++: Observer-Makros:
x = new Class(y); x = NEW(x,Class,(y));
x = new Class[N]; x = NEW_ARRAY(x,Class,N,());
x = new Class[N](5,6); x = NEW_ARRAY(x,Class,N,(5,6));
return new Class(); return NEW(as_you_like,Class,());
delete x; DELETE(x);
delete [] x; DELETE_ARRAY(x,Class,N);
printf(''Hello, world!''); debug_printf((''Hello, world!''));

 


Beispiel A.1:

Der folgende Programmcode macht deutlich, wie die Änderungen beim Einsatz des Observers ausfallen müssen. Das Programm erzeugt ein Integer-Array und löscht dieses gleich wieder:

// Standard C++                 // mit Observer-Makros
void main() {                   void main() {
  int *arr = new int[100];        int *arr = NEW_ARRAY(arr, int, 100, ());
  ...                             ...
  delete [] arr;                  DELETE_ARRAY(arr, int, 100);
}                               }


Ist beim Compilieren das Makro OBSERVEMEM definiert (über das Compiler-Argument -DOBSERVEMEM), so werden die erweiterten Makros aufgenommen. Wird das Makro nicht gesetzt, so werden die normalen new und delete Aufrufe eincompiliert. Dies ist wichtig, da man so bei einer endgültigen Programmversion ohne Änderung des Programmcodes sämtliche Debug-Informationen entfernen kann.

Das debug_printf-Makro wird ohne das Makro OBSERVEMEM einfach auskommentiert, mit gesetztem Makro wird eine eigene Ausgabemethode gestartet. Diese garantiert bei einer Ausgabe in eine Datei (s.u.), daß bei einem Abbruch mit core-File auch die letzten Meldungen noch ausgegeben werden. Bei einer Standard-Ausgabe mit printf kann hier durch die Pufferung von stdout etwas verloren gehen (insbesondere bei Benutzung von Pipes).

Zur Laufzeit des Programms kann der Benutzer entscheiden, wie er den Speicherschutz anwenden will, gesteuert durch eine Environmentvariable $OBSERVEMEM:

OBSERVEMEM Funktion
nicht gesetzt Speicherprüfung und Debug-Ausgabe inaktiv
- aktiviert, Ausgabe auf stdout
XXX aktiviert, Ausgabe in XXX.mem und XXX.debug

Hat der Benutzer die Ausgaben in eine Datei umgelenkt, so steht in der Datei XXX.mem der komplette Speicherverlauf des Programms, und in XXX.debug die Ausgaben von debug_printf. Jetzt kann der Ablauf der Speicherverwaltung auf Korrektheit geprüft werden. Zu diesem Zweck existiert ein Perl-Programm observemem.pl.

Das Skript bekommt den Namen der Speicherdatei übergeben, also:

        observemem.pl XXX.mem

Jetzt wird die Datei parsiert und mögliche Fehler werden ausgegeben. Dazu zählen nicht freigegebene Bereiche, doppelt freigegebene Bereiche, new/delete-Paare die Skalare anfordern und Arrays freigeben (und umgekehrt), sowie Schreiben über die Arraygrenzen hinaus.

Mit dem Parameter -m kann eine Speicherkarte ausgegeben werden, d.h., die Variablen werden so sortiert ausgegeben, wie sie im Speicher liegen. Der Parameter -c prgfile corefile bezieht bei einem Programmabsturz das erzeugte core-File mit in die Betrachtung ein. Dazu muß allerdings der GNU-Debugger gdb installiert sein.

Mit Hilfe dieses Verfahrens kann man sicherstellen, daß keine Speicherlecks im Programm vorhanden sind, weiterhin ist ein korrekter Zugriff auf die Arrays sichergestellt. Nach der Entwicklungsphase muß das Programm nur einmal komplett ohne das OBSERVEMEM-Makro compiliert werden, Umstellungen im Code sind nicht mehr nötig.



Carsten Wilhelm
Sat Jun 20 09:42:29 MEST 1998