Debuggen von linalg

Das Programm linalg enthielt am Anfang natürlich einige Fehler, die wir jetzt mit dem X-Window-fähigen Source-Debugger CXdb aufspüren wollen. In diesem Abschnitt sollen die Möglichkeiten des Debuggers nicht systematisch dargestellt werden - dies bleibt einem eigenen Kapitel vorbehalten -, sondern es werden die Ideen und Befehle beschrieben, die zum Debuggen unseres Beispielprogramms nötig sind.
  1. Die fehlerhafte Ur-Version von linalg bekommen wir aus den RCS-Files mit ''make VERSION=1.1 source''. Zunächst muß das fehlerhafte Programm für den CXdb übersetzt werden. Dazu setzen wir die Variablen FFLAGS und LDFLAGS im Makefile auf ''-cxdb'' und übersetzen alles neu. Nach dem Aufruf von linalg erhalten wir die Fehlermeldung ``Floating point exception'' und ein Stack-Listing, dem man nach etwas Suchen entnehmen kann, daß der Fehler in der Routine GAUSSJ aufgetreten ist.
    Wir rufen nun den Debugger auf mit ``cxdb linalg''. Es erscheinen zwei Fenster: Ein Command-Fenster, in dem Debug-Kommandos eingegeben und die Antworten ausgegeben werden und das Menüs und Knöpfe für viele wichtige Debug-Befehle enthält, und das Source-Fenster, das den Source-Code des aktuellen Files enthält, im Moment den von main.f. Ein weiteres Fenster, das Help-Fenster, erscheint, wenn man im Command-Fenster den Help-Knopf drückt oder direkt das Kommando help eingibt. Hier kann man zu jedem Debug-Befehl und zu vielen Konzepten des CXdb ausführliche Informationen erhalten, über das Menü ``Topics'' Listen aller Suchbegriffe einsehen oder im Menü ``HelpWindow'' ein Online-Tutorial starten.
    Wir starten das Programm linalg im Debugger durch Eingabe des Kommandos ``run''. Zunächst erscheint ein neues Fenster, das Prozeß-Fenster, über das alle Ein- und Ausgaben des laufenden Programms abgewickelt werden. Bevor es aber zu irgendwelchen Ausgaben kommt, stoppt das Programm, und der CXdb informiert uns im Command-Fenster, daß in Zeile 69 der Routine GAUSSJ im File gaussj.f eine ``Floating point exception'' aufgetreten ist. An dieser Stelle befinden wir uns in einer L-Schleife (Z.68-70) in einer LL-Schleife (Z.64-75) innerhalb einer I-Schleife (Z.23-76).

    Um der Sache auf den Grund zu gehen, sehen wir uns zunächst die Werte aller lokalen Variablen an mit ''info locals''. U.a. sehen wir, daß

            I   = 1
            LL  = -2147091208
            L   = 86
    
    Der Wert für LL ist merkwürdig, denn LL sollte von 1 bis N laufen, und mit ``print N'' überzeugen wir uns von
            N   = 100
    
    Um uns anzusehen, wann LL diesen seltsamen Wert bekommt, setzen wir einen ''Tracepoint'' an den ersten Befehl der LL-Schleife (Z.65), d.h. wir wollen jedesmal, wenn diese Stelle passiert wird, einige Informationen erhalten. Wir definieren dazu:
            trace line 65 {echo/n "Zeile 65: LL = "; print LL; resume;}
    
    Damit werden jedesmal, wenn diese Stelle passiert wird, die Kommandos in den geschweiften Klammern ausgeführt, d.h. es wird - ohne anschließendes Newline - ein Text ausgegeben (echo/n), dann der Wert der Variablen LL angezeigt (print), und schließlich mit der Programm-Ausführung fortgefahren (resume). Ohne ''resume'' würde bei Erreichen des Tracepoint das Programm angehalten. Der Tracepoint wird durch ein Symbol im Source-Fenster angezeigt.

    Wir starten das Programm nun neu mit dem Kommando ''run''. Da das Programm vom letzten Lauf noch aktiv ist, fragt CXdb nach, ob der entsprechende Prozeß abgebrochen werden soll. Wir antworten mit ''y'' für ja. Als Ergebnis des Trace erhalten wir:

            Zeile 65: LL = (INTEGER*4) 1
            Zeile 65: LL = (INTEGER*4) 2
            Zeile 65: LL = (INTEGER*4) 3
            Zeile 65: LL = (INTEGER*4) 4
    
    Danach erscheint wieder die alte Fehlermeldung. Daraus folgt, daß sich zwischen dem Tracepoint (Z.65) und dem Fehler (Z.69) der Wert von LL geändert hat, obwohl dort nur die Variablen DUM und A auftauchen. Wir sehen uns den Inhalt von A an mit ``print A''. CXdb gibt standardmäßig maximal 20 Werte eines Arrays auf einmal aus, in unserem Fall also nur den Anfang der ersten Zeile. Aber auch hier sieht man schon, daß darunter gigantisch große Zahlen sind, obwohl A am Anfang nur Zufallszahlen zwischen -1 und 1 enthalten soll. Mehrere Ursachen sind denkbar: Im letzten Fall müßte nicht einmal ein Fehler vorliegen. Es könnte ja sein, daß die Zufallszahlen in A in GAUSSJ zu numerischen Instabilitäten geführt haben. Allerdings ist der verwendete Algorithmus insbesondere wegen der vollständigen Pivot-Suche (Vertauschen von Zeilen und Spalten, um mit dem jeweils größten Element weiterzuarbeiten) sehr stabil, so daß diese Möglichkeit zumindest sehr unwahrscheinlich ist.
    Um zu sehen, wann A so große Werte bekommt, wollen wir das Programm schrittweise durchgehen. Dazu müssen wir zunächst einen Haltepunkt (''breakpoint'') am Anfang setzen, damit das Programm nach der Initialisierungsphase anhält:
            break routine LINALG
    
    Nach ''run'' (und der üblichen Abfrage) bleibt das Programm in Zeile 46 von main.f , der ersten ausführbaren Zeile, stehen. Der Breakpoint wird im Source-Fenster durch ein ''B'' vor der Zeile angezeigt. Mit den Kommandos ''step'' und ''next'' kann man nun in Einzelschritten durch das Programm gehen, wobei ''next'' einen Unterprogramm-Aufruf als einen Schritt ansieht und komplett ausführt, während ''step'' in das Unterprogramm hineingeht. Mit zwei next-Schritten führen wir die Routine GETVEC aus und sehen uns mit ''print A'' die ersten 20 Werte von A an: Sie liegen alle im richtigen Bereich. Mit drei weiteren next-Kommandos gehen wir bis vor den Aufruf von GAUSSJ. Die Werte von A sind erwartungsgemäß unverändert. Nun gehen wir mit ''step'' in die Routine GAUSSJ hinein. ''print A'' zeigt, daß jetzt A falsche Werte hat, es wird also nicht richtig an GAUSSJ übergeben. Mit ''info args'' erhalten wir Informationen über die Argumente von GAUSSJ; insbesondere sehen wir, daß A hier den Typ $\mbox{REAL}*\mbox{4}$ hat, im Hauptprogramm aber als $\mbox{REAL}*\mbox{8}$ deklariert ist. Damit ist der erste Fehler gefunden: Die Argumente und Variablen in GAUSSJ sind nicht deklariert worden (die Routine wurde unverändert aus den ''Numerical Recipes'' übernommen).


    Abhilfe: alle Variablen in gaussj deklarieren.


  2. Die entsprechend korrigierte Version von linalg hat die Nummer 1.2, wir erhalten sie mit ''make VERSION=1.2 source''. Erneutes ''make'' und Starten von linalg bewirkt jetzt:
            PAUSE: SINGULAR MATRIX
    
    Wir starten wieder den Debugger mit ''cxdb linalg'' und das Programm selbst mit ''run''. Das Proceß-Fenster erscheint mit der PAUSE-Zeile und einer Eingabe-Einforderung. Wir antworten aber nicht dort - das wurde den Prozeß beenden -, sondern im Command-Fenster mit ''Control-c'', um den Prozeß anzuhalten. Das Source-Fenster zeigt die augenblickliche Position an: Das Programm steht in GAUSSJ in Zeile 41. Um zu sehen, warum die IF-Abfrage in Z.40 erfüllt wurde, sehen wir uns mit ''info locals'' die entsprechenden Variablen an:
            K = (INTEGER*4) 51
            IPIV = INTEGER*4(1:50) 0x8004c040
    
    Der Fehler ist diesmal leicht zu finden: IPIV ist deklariert als 50-elementiges Feld, aber in Zeile 40 wird IPIV(51) abgefragt. Der Wert von K geht auf die Schleife in Z.33-43 zurück, in der K von 1 bis N=100 läuft. IPIV dagegen ist über den Parameter NMAX=50 deklariert.


    Abhilfe: Parameter NMAX vergrößern (mindestens NMAX=100)


  3. Die korrigierte Version 1.3 läuft durch GAUSSJ und die anschließenden Testroutinen durch, liefert aber eine ''Floating Point Exception'' in LUDCMP. Nach den bisherigen Erfahrungen ahnen wir die Fehlerquelle sofort: In LUDCMP - und in LUBKSB - werden die Variablen nicht deklariert, es kommt also wieder zu Inkonsistenzen ( $\mbox{REAL}*4 \Longleftrightarrow
\mbox{REAL}*8$).


    Abhilfe: Für alle Variable Deklarationen einfügen.


    Aus 1 - 3 lernen wir, daß man beim Übernehmen von Routinen aus anderen Quellen sehr vorsichtig sein muß, ob sie implizite Voraussetzungen enthalten. Besonders kritisch sind dabei die Typen von Argumenten (und evtl. passenden lokalen Variablen) und die Größen von Parametern.
  4. Die letzte fehlerhafte Version ist 1.4. Sie läuft zwar ganz durch, aber der Test der durch LU-Zerlegung bestimmten inversen Matrix liefert eine große Abweichung vom erwarteten Ergebnis, während die Lösung des Gleichungssystems stimmt.
    Wir setzen einen Breakpoint vor den 2. Aufruf von TESTMT (''break line 79''), lassen das Programm bis dahin laufen (''run'') und sehen uns dann die linke obere Ecke der Matrix B an (''print B(1..5, 1..5)''). Einige Werte (z.B. B(2,2)) liegen außerhalb des Bereichs [-1,1], B kann also nicht mit der Ausgangsmatrix identisch sein. Ein Blick in main.f zeigt, daß B tatsächlich in Z.72 durch LUDCMP LU-zerlegt wurde. In TESTMT wird also nicht B, sondern A benötigt.
Nach dieser Änderung läuft das Programm problemlos durch. Es wird als Version 2.1 zur Ausgangsbasis für die nächsten Abschnitte.

previous    contents     next

Peter Junglas 18.10.1993