51. Debugging I

Gegeben sei folgendes Programm:

#include <stdio.h>
#include "liste.h"

int main(int argc, char **argv)
{
  List *l=list_new();
  char *val;

  argv++;
  while (*argv) {
    list_append(l, *argv++);
  }
  list_sort(l);
  while ((val=list_getAt(l, 0))) {
    printf("%s\n", val);
    list_deleteAt(l, 0);
  }
  list_deleteAt(l, 0);
  list_free(l);
  return 0;
}

Wir müssen das mit dem Listenmodul linken, als Regeln für Make bieten sich an:

%: %.o
  $(CC) -o $@ $(LDFLAGS) $(CFLAGS) $^

crash: crash.o liste.o

(das Programm soll in crash.c stehen).

Beim Ausführen des Programms passiert folgendes:

examples> crash dab bad abd
abd
bad
dab
Segmentation fault
Exit 139

– das Programm versucht, auf Speicher zuzugreifen, der ihm nicht gehört.

Will man solche Fehler finden (Debugging), helfen häufig Programme, die Debugger heißen.

Man hört oft, das Wort “Bug” (Käfer, Wanze) für einen Fehler in Computerprogrammen komme daher, dass in einem der ersten Rechner ein fast unfindbarer Fehler durch eine wirkliche Wanze oder Motte, die es sich in einem seiner Relais bequem gemacht hatte, verursacht wurde. Das ist so nicht zutreffend, vgl. den ensprechenden Eintrag im Jargon File.

Will man nicht Maschinensprache debuggen, muss der Debugger wissen, welcher Code aus welcher Quellzeile kommt. Diese Information schreibt der Compiler nur auf Anfrage, und zwar mit dem -g-Flag.

Für Nicht-Unix-Compiler mag das anders aussehen, aber auch diese schreiben Debugging-Informationen in der Regel nicht automatisch, schon, weil das eine endlose Platzverschwendung wäre.

Benutzt man Make wie hier empfohlen, fügt man einfach -g zu den CFLAGS und lässt make clean; make laufen.

Wenn ein Programm unter Unix abstürzt, schreibt es normalerweise eine Datei core, in der steht, wie der Speicher des Programms am Ende aussah.

Tatsächlich schalten viele Administratoren und Distributionen das aus, weil die meisten Menschen heute nicht mehr viel mit diesen Coredumps anfangen können und sie nur Platz auf der Platte verbrauchen. Wenn nach einem Segfault oder ähnlichem kein core auf der Platte liegt, kann man durch das Kommando ulimit -c unlimited (o.ä., je nach Shell; es kann sein, dass ihr unlimited nicht dürft, probiert es dann mit einer großen Zahl, vielleicht 4000000) dafür sorgen, dass Coredumps geschrieben werden.

Nach einem Coredump kann gdb sagen, wo der Absturz passiert ist:

examples > gdb crash core
GNU gdb 5.1
...
#0  in list_deleteAt (l=0x8049e90, index=0) at liste.c:110
110         l->first = toDel->next;
(gdb)

und auch, wie das Programm zur Absturzstelle gekommen ist:

(gdb) where
#0  in list_deleteAt (l=0x8049e90, index=0) at liste.c:110
#1  in main (argc=4, argv=0xbffff934) at crash.c:18
#2  in __libc_start_main (main=0x8048540 <main>, argc=4,
    ubp_av=0xbffff924, init=0x8048374 <_init>,...
    at ../sysdeps/generic/libc-start.c:129

So wissen wir immerhin mal, dass der Absturz in Zeile 110 in list.c in der Funktion list_deleteAt passiert ist, und dass der Aufruf aus aus der Zeile 18 in crash.c kam – das ist der letzte Aufruf. Man überlegt sich leicht, dass die Liste zu diesem Zeitpunkt leer ist, und das sollte in der Regel schon reichen, um den Fehler zu finden.

Aber der gdb kann noch viel mehr. So kann man sich z.B. zunächst die Umgebung der Absturzstelle ansehen:

(gdb) list
105     {
106       Node *toDel;
107
108       if (index==0) {
109         toDel = l->first;
110         l->first = toDel->next;
111         if (!l->first) {
112           l->last = NULL;
113         }
114       } else {

und sich Variablen ansehen:

(gdb) print index
$1 = 0
(gdb) print toDel
$2 = (Node *) 0x0
(gdb) print l->first
$3 = (Node *) 0x0

All das passiert vorläufig in der Funktion, in der der Absturz passiert ist. Wir können aber auch am Stack eine Funktion “raufgehen”:

(gdb) up
#1  0x080485e5 in main (argc=4, argv=0xbffff934) at crash.c:18
18              list_deleteAt(l, 0);
(gdb) list
13              list_sort(l);
14              while ((val=list_getAt(l, 0))) {
15                      printf("%s\n", val);
16                      list_deleteAt(l, 0);
17              }
18              list_deleteAt(l, 0);
19              list_free(l);
20              return 0;
21      }

und sich Variablen in diesem Kontext ansehen:

(gdb) print argc
$4 = 4
(gdb) print *argv
$5 = 0x0

– dass argv ein Nullpointer ist, zeigt uns wieder, dass die erste Schleife durchgelaufen ist.

Auf diese Weise lässt sich häufig rekonstruieren, wie es zu einem Absturz kam.

Der gekonnte Einsatz von Debuggern ist letztlich eine Kunst, die man durch viel Erfahrung lernt – und manchmal ist es besser, einfach ein printf an eine strategisch gute Position zu legen.


Markus Demleitner

Copyright Notice