Fehler in Software zu finden, ist ein hartes Brot. Hilft das Programm dabei mit, kann man sich das mühsame Hantieren mit dem Debugger eventuell ersparen.
Manche Programmierfehler bleiben lange unentdeckt. Einer davon ist das Kopieren überlappender Speicherbereiche mit memcpy oder strcpy, das der C-Standard seit über 10 Jahren ausdrücklich verbietet. Aus gutem Grund, denn optimierte Versionen der Funktionen kopieren nicht mehr Byte für Byte von links nach rechts. Daher nehmen sie es eventuell übel, wenn der Programmierer einen Aufruf wie
strcpy(string, string + n);
verwendet hat, um n Zeichen aus einem String zu entfernen, und produzieren stattdessen Buchstabensalat. Leider ist dieses Idiom gängiger, als es sein sollte: Unter Linux findet man es unter anderem in der GNU-Shell bash – wo es allerdings keine gravierenden Auswirkungen hat –, im Assembler yasm und in den X.org-Tools imake und makedepend. Außerdem ertappte der Autor die OpenGL-Implementierung Mesa und Adobes Flash Player für Linux beim falschen, wenn auch folgenlosen Gebrauch von memcpy.
Beheben lassen sich die Fehler relativ einfach. In den meisten Fällen genügt es, memcpy durch memmove und strcpy(s1, s2) durch die Sequenz
memmove(s1, s2, strlen(s2) + 1);
zu ersetzen und das Programm neu zu kompilieren. Steht der Quellcode nicht zur Verfügung, muss man sich jedoch anderer Methoden bedienen. „Unixoide“ Betriebssysteme bieten oft mit der Environment-Variablen LD_PRELOAD die Möglichkeit, Programmen beim Start zusätzliche Shared Libraries unterzujubeln. Die darin enthaltenen Funktionen ersetzen dann die gleichnamigen aus der Standard-Laufzeitbibliothek. Der Memory-Debugger Electric Fence und sein Nachfolger DUMA (Detect Unintended Memory Access) etwa nutzen LD_PRELOAD, um Aufrufe von malloc oder free auf ihre eigenen Versionen der Funktionen umzuleiten.
Da damit ein Sicherheitsrisiko verbunden ist, funktioniert der Preload-Mechanismus nicht, wenn das Programm per setuid-Bit mit den Rechten eines anderen Nutzers läuft. Unter Linux kann der Administrator eine Bibliothek trotzdem laden, indem er ihren Namen in die Datei /etc/ld.so.preload einträgt. Allerdings verwenden dann alle Programme die Ersatzroutinen.
Bevor man einen Fehler beseitigen kann, muss man ihn finden. Dabei leistet der Preload-Mechanismus ebenfalls gute Dienste. Listing 1 zeigt einen einfachen Ersatz für strcpy, der die übergebenen Parameter vor der Ausführung überprüft. Tritt ein Fehler auf, gibt check eine Meldung aus und beendet das Programm mit abort. Der Nutzer kann den erzeugten Core-Dump anschließend in den Debugger laden und sich die Stelle des Aufrufs zeigen lassen. Die Routine check ist bewusst generisch gehalten, sodass sie sich auch zum Überprüfen der Argumente von memcpy, strcat oder strncpy nutzen lässt.
Soll das Programm weiterlaufen, empfiehlt es sich, mit fork einen zweiten Prozess zu erzeugen, der die Fehlermeldung ausgibt und sich anschließend mit abort beendet. Unter Umständen ist es auch sinnvoll, die Fehlermeldung nicht auf das Terminal des Nutzers zu schicken, sondern in eine eigens dafür geöffnete Protokolldatei zu schreiben oder an den Logging-Daemon syslogd zu senden.
Eine aussagefähigere Fehlermeldung kann selbstverständlich nicht schaden. Zumindest sollte sie den Namen und die Argumente der aufgerufenen Funktion enthalten. Wer zum Kompilieren gcc nutzt, kann obendrein die Rücksprungadresse der Funktion ausgeben lassen, die sich mit __builtin_return_address(0) abfragen lässt. __builtin_frame_address(0) liefert einen Zeiger auf den Stack Frame der aufrufenden Funktion, sodass sich die Aufrufhistorie nachvollziehen lässt (siehe Listing 2).
Nutzer der glibc müssen sich die Mühe nicht machen: Sie können die Rücksprungadressen mit der Funktion backtrace abrufen. Außerdem liefert die GNU-Laufzeitbibliothek mit backtrace_symbols die Namen der dazugehörigen Funktionen, sofern der Übersetzer die Symboltabelle des Programms nicht mit strip entfernt hat.
(mr)
Alle Links: www.ix.de/ix1102141
static void
check(const char *dst, size_t dlen,
const char *src, size_t slen)
{
if (dst + dlen <= src || src + slen <= dst)
return; /* keine Überlappung */
fprintf(stderr, "Fehler!\n");
abort();
}
char*
strcpy(char *dst, const char *src)
{
size_t len = strlen(src) + 1;
check(dst, len, src, len);
memmove(dst, src, len);
return dst;
}
struct frame {
struct frame *frm;
void *ret;
};
void
stacktrace(void) {
struct frame *p = __builtin_frame_address(0);
int i = 0;
while (p) {
printf("%d: called from %p\n", i++, p->ret);
p = p->frm;
}
}
Heft bestellen Dieser Text ist der Zeitschriften-Ausgabe 02/2011 von iX entnommen. Das Heft kann online portokostenfrei bestellt werden.
iOS, Android, Windows Phone 7 und HTML5 - das neue Sonderheft von heise Developer führt Einsteiger und Profis in die Programmierung mobiler Geräte ein.