Humpeln zur Diagnose (Linux-Magazin, April 2005)

Debugger werden von vielen als Teufelszeug verdammt. Sie erweisen sich aber manchmal als letzte Rettung, um schwer verständlichem Code auf die Schliche zu kommen oder versteckte Fehler zu finden. Auch Perl hat einen -- sogar eingebaut.

Linus Torvalds mag keine Debugger und auch keine Programmierer, die sie verwenden. Zu leichtfertig lässt sich ein Stück Code zusammenschustern und mit einem Debugger geradebiegen. Allerdings macht sich kaum ein Chaosprogrammierer Design-Gedanken. Das rächt sich dann oft später, wenn die Software schwer wart- oder erweiterbar ist.

Gewissenhaft eingebettetes Logging macht Debugger in vielen Fällen überflüssig. Wie in [3] ausgeführt, hilft Log::Log4perl, einen maßgeschneiderten Debugger schon in die Applikation einzubauen und bei Bedarf per Fernsteuerung zu aktivieren.

Manchmal hilft aber alles nichts. Was tun, wenn ein Programmstück unerwartet reagiert, die Dokumentation darüber nichts enthält, und der (natürlich von jemand anderem geschriebene) Code zu kompliziert ist, um seinen Ablauf durch Studieren der Listings zu verstehen?

Perl enthält von Haus aus einen Debugger, der es recht schnell erlaubt, mit Breakpoints, Actions und Watchpoints Fehler einzukreisen.


Probelauf

Listing wsrv zeigt einen praktischen Fünfzeiler, der die Marke des Webservers anzeigt, der hinter einem URL steckt:

    $ wsrv http://sun.com
    Sun Java System Web Server 6.1

Soll das Skript stattdessen im Debugger ablaufen, setzt man einfach ein perl -d vor den Skriptnamen und alle Kommandozeilenargumente:

    $ perl -d wsrv http://microsoft.com
    Loading DB routines from perl5db.pl version 1.27
    Editor support available.
    Enter h or `h h' for help, or `man perldebug' for more help.
    main::(wsrv:7): my $url = shift or die "usage $0 url";
    DB<1>

Um die angezeigte erste Zeile des Skripts auszuführen, die den URL aus den Kommandozeilenargumenten @ARGV extrahiert, gibt der Bediener den Befehl n (next) ein:

    DB<1> n
    main::(wsrv:8): my (@fields) = head($url) or 
    main::(wsrv:9):     die "Fetch failed";

Die erste Zeile wurde kommentarlos ausgeführt. Da beim Debuggeraufruf auf der Kommandozeile ein URL angegeben war, liegt dieser nun in der Variablen $url. Die nächste angezeigte ausführbare ``Zeile'' oben besteht aus den Source- Zeilen 8 und 9 von wsrv. Statt sie mit n vollständig auszuführen, wollen wir nun mit s (step) in Einzelschritten vorgehen. Prompt steigt der Debugger in die in Zeile 9 aufgerufene Funktion head hinab:

    DB<1> s
    LWP::Simple::head(.../LWP/Simple.pm:70):
    70:     my($url) = @_;
    DB<1>

Das Kommando l (list) verschafft einen Überblick über die nächsten Zeilen in LWP::Simple:

    DB<1> l
    70==> my($url) = @_;
    71:   _init_ua() unless $ua;
    72 
    73:   my $request = HTTP::Request->new(HEAD => $url);
    74:   my $response = $ua->request($request);
    ...

Um im Code weiter nach unten zu fahren, ohne ihn allerdings auszuführen, genügt ein weiteres l-Kommando. Alternativ ginge auch l 70+20 (20 Zeilen ab Zeile 70 anzeigen) oder l 70-100 (Zeilen 70 bis 100). Die nächste ausführbare Zeile wird oben mit ==> angezeigt. Die Eingabe eines Punktes lässt die Listinganzeige wieder zum Ausgangspunkt zurückkehren, wenn man sich verfahren hat. Statt nun weiter mit n oder s in der Funktion head() herumzufahren, wird der Debugger mit r (return) angewiesen, die aktuelle Funktion bis zum Ende auszuführen und anschließend sofort im Hauptprogramm anzuhalten:

    DB<1> .
    LWP::Simple::head(.../LWP/Simple.pm:70):
    70:         my($url) = @_;
    DB<1> r
    list context return from LWP::Simple::head:
    0  'text/html'
    1  16144
    2  1107018115
    3  1107028115
    4  'Microsoft-IIS/6.0'
    main::(wsrv:10):        print "Server: $fields[4]\n";

Freundlicherweise zeigt der Debugger sogar die Rückgabewerte der Funktion head() an, unmittelbar vor der nächsten ausführbaren Zeile, der print-Funktion im Hauptprogramm. Welchen Wert führt nun das Array-Element $fields[4]? Der Befehl p (print) des Debuggers fördert ihn zutage, noch bevor die print-Zeile des Hauptprogramms ihn preisgibt:

    DB<1> p $fields[4]
    Microsoft-IIS/6.0

Um den Inhalt des Arrays @fields auszugeben, könnte man entsprechend p @fields verwenden, allerdings wäre die Ausgabe nicht gerade augenfreundlich. Für kompliziertere Datenstrukturen als Skalare bietet der Debugger deswegen die Funktion x:

    DB<2> x @fields
    0  'text/html'
    1  16144
    2  1107021419
    3  1107031419
    4  'Microsoft-IIS/6.0'

Ähnliches gilt für Hashes. Da das Programm keinen verwendet, definierten wir einfach schnell einen, ebenfalls im Debugger:

    DB<3> %h = (donald => 'duck', 
                klaas  => 'klever')
    DB<4> x %h
    0  'donald'
    1  'duck'
    2  'klaas'
    3  'klever'

Wer statt der Array-artigen Anzeige lieber Key-Value-Paare möchte, übergibt stattdessen eine Hash-Referenz an x:

    DB<5> x \%h
    0  HASH(0x837a5f8)
     'donald' => 'duck'
     'klass'  => 'klever'

Während der letzten Befehle hat sich die laufende Nummer des Prompts (DB>x<) nach oben verändert. Für alle nicht-trivialen Befehle führt der Debugger eine History-Liste, die sich mit dem Kommando H anzeigen lässt:

    DB<6> H
    5: x \%h
    4: x %h
    3: %h = (donald => 'duck', klaas  => 'klever')
    2: x @fields
    1: p $fields[4]

Um zum Beispiel das $field[4]-Element nochmals auszugeben, genügt ein Ausrufezeichen, gefolgt, von der Nummer des History-Eintrags:

    DB<6> !1
    p $fields[4]
    Microsoft-IIS/6.0

Mit diesem kleinen Rüstsatz an wichtigen Debugger-Befehlen lassen sich nun schon kompliziertere Aufgaben anpacken.

Listing 1: wsrv

    01 #!/usr/bin/perl -w
    02 ###########################################
    03 # wsrv - Display a URL's web server
    04 # Mike Schilli, 2005 (m@perlmeister.com)
    05 ###########################################
    06 use LWP::Simple;
    07 my $url = shift or die "usage $0 url";
    08 my (@fields) = head($url) or 
    09     die "Fetch failed";
    10 print "Server: $fields[4]\n";

Echtes Problem

Erzeugen wir mal ein neues fiktives Modul Irgendwie::Sowieso und bereiten es für einen CPAN-Release vor. Dazu gehört eine Datei Makefile.PL, nach Listing 1 und eine Moduldatei lib/Irgendwie/Sowieso.pm, die vielleicht etwas Dokumention der Art

    =head1 NAME
        Irgendwie::Sowieso - blah blah blah
    =head1 SYNOPSIS
        use Irgendwie::Sowieso;

enthält. Ein Aufruf von

    perl Makefile.PL

führt dann zu der etwas kryptischen Fehlermeldung

    WARNING: Setting ABSTRACT via file 'lib/Irgendwie/Sowieso.pm' 
    failed at ExtUtils/MakeMaker.pm line 606

Was ist passiert? Keine Ahnung. Hinter Makefile.PL steckt ExtUtils::MakeMaker, ein Urgestein der Perl-Programmierung, dessen Innereien nicht ganz einfach zu verstehen sind.

Listing 2: Makefile.PL

    01 ####################################
    02 # Makefile.PL for Irgendwie::Sowieso
    03 ####################################
    04 use ExtUtils::MakeMaker;
    05 WriteMakefile(
    06   'NAME' => 'Irgendwie::Sowieso',
    07   'VERSION_FROM' => 
    08          'lib/Irgendwie/Sowieso.pm',
    09   'PREREQ_PM' => {},
    10   (
    11     $] >= 5.005
    12     ? (
    13       ABSTRACT_FROM => 
    14          'lib/Irgendwie/Sowieso.pm',
    15       AUTHOR => 
    16   'Mike Schilli <m@perlmeister.com>'
    17       ) : ()
    18   ),
    19 );

Aber mit der Option -d ruft perl den Debugger auf:

    perl -d Makefile.PL

Da die Warnung oben die Datei ExtUtils/MakeMaker.pm und die Zeile 606 als Fehlerquelle angibt, springen wir einfach mal mit dem Kommando f (File) dorthin:

    DB<1> f ExtUtils/MakeMaker.pm

Anschließend setzen wir in Zeile 606 einen Breakpoint und weisen dann den Debugger mit c (continue) an, bis dorthin fortzuschreiten:

    DB<2> b 606
    DB<2> c
    606: push @{$self->{RESULT}}, 
         $self->nicetext($self->$method( %a ));

Statt erst den Breakpoint zu setzen und anschließend mit c dorthin zu fahren, hätte c 606 gleich losgelegt und in 606 gestoppt. Allerdings wäre dann in 606 kein permanenter Breakpoint, der später nochmal genutzt werden könnte.

Der oben in der aktuellen Zeile angezeigte push-Befehl scheint das Ergebnis eines Methodenaufrufs ans Ende eines Arrays zu hängen.

Es wäre interessant, zu wissen, welche Methode mit $method aufgerufen wird:

    DB<3> p $method
    post_initialize

Hmmm. Nie gehört. Der Perl-Debugger schreitet mit n (Next) eine Zeile voran, probieren wir das einfach einmal:

    DB<4> n

Keine Reaktion. n führte die Zeile aus, und, siehe da, die Warnung wurde nicht ausgegeben. Anscheinend führt der MakeMaker die Zeile mehrfach aus und erst beim n-ten Mal tritt der Fehler auf.

Kein Problem: Bevor es mit c (continue) in die nächste Iteration geht (gestoppt vom Breakpoint in 606), definieren wir mit a eine Aktion in dieser Zeile, nämlich $method auszugeben:

    DB<4> a 606 print("$method\n");

Diese Aktion wird der Debugger nun jedesmal ausführen, wenn er an Zeile 606 vorbeirauscht, auch wenn es nicht in Einzelschritten vorwärts geht, sondern das Programm in voller Geschwindigkeit abläuft. Weiter geht's mit c für continue:

    DB<4> c
        606: push @{$self->{RESULT}}, 
             $self->nicetext($self->$method( %a ));
    platform_constants

Da der Breakpoint in Zeile 606 immer noch gesetzt ist, hält der Debugger in der nächsten Runde prompt wieder an. Noch immer taucht die gesuchte Warnung nicht auf. Also den Breakpoint in Zeile 606 mit B 606 löschen und das Programm mit c weiterlaufen lassen:

    DB<4> B 606
    DB<5> c
    ...
    staticmake
    test
    ppd
    WARNING: Setting ABSTRACT via file 'lib/Irgendwie/Sowieso.pm' failed

Also die ppd-Methode löst die Warnung aus. Leider sind wir jetzt über's Ziel hinausgeschossen, aber keine Bange, mit R fängt das Programm wieder von vorne an und stoppt am Anfang:

    DB<5> R
    ...
    main::(Makefile.PL:14): );

Setzen wir einfach nochmal einen Breakpoint in Zeile 606 von ExtUtils/MakeMaker.pm, aber diesmal mit einer Bedingung verknüpft:

    DB<5> f ExtUtils/MakeMaker.pm
    DB<6> b 606 $method eq "ppd"
    DB<7> c

Der Breakpoint wird den Debugger nun nur dann anhalten, wenn die Variable $method den String "ppd" enthält. Und, richtig, das Programm läuft los, und wieder hält der Debugger in Zeile 606 an, diesmal enthält $method den Wert ``ppd'':

    606: push @{$self->{RESULT}}, $self->nicetext($self->$method( %a ));
    DB<7> p $method
    ppd

Welche Methoden kann $self ausführen? Das Kommando m mit $self als Argument zeigt eine ganze Portion an:

    DB<8> m $self
    ...
    via MM -> ExtUtils::MM -> ExtUtils::MM_Unix: post_initialize
    via MM -> ExtUtils::MM -> ExtUtils::MM_Unix: postamble
    via MM -> ExtUtils::MM -> ExtUtils::MM_Unix: ppd
    ...

Aha, die Methode ppd ist anscheinend im Modul ExtUtils::MM_Unix definiert. Setzen wir also das Programm mit dem Kommando c fort, lassen es diesmal aber abbremsen, sobald die Methode ExtUtils::MM_Unix::ppd aktiv ist:

    DB<9> c ExtUtils::MM_Unix::ppd
    ExtUtils::MM_Unix::ppd(ExtUtils/MM_Unix.pm:3322):
    3322:       my($self) = @_;

Der Debugger steht jetzt in der ersten Zeile der Methode ppd im Modul ExtUtils::MM_Unix::ppd. Eine kurze Orientierung mit l zeigt, dass ppd die Methode parse_abstract() aufruft:

    DB<10> l
    3322==>     my($self) = @_;
    3323 
    3324:       if ($self->{ABSTRACT_FROM}){
    3325:           $self->{ABSTRACT} = 
                        $self->parse_abstract($self->{ABSTRACT_FROM}) or

Das klingt doch verlockend. Mit dem gleichen Verfahren, c funktion, wird der Debugger angewiesen, weiterzulaufen und in der ersten Zeile von parse_abstract anzuhalten:

    DB<11> c parse_abstract
    ExtUtils::MM_Unix::parse_abstract(ExtUtils/MM_Unix.pm:3045):
    3045:       my($self,$parsefile) = @_;

Die nächsten 20 Zeilen zeigt das Kommando

    DB<12> l +20

und verrät folgenden regulären Ausdruck, mit dem Makefile.PL den 'Abstract' aus dem Modul holt:

    3057:           next unless /^($package\s-\s)(.*)/;

Zur Kontrolle setzen wir einfach mit dem Kommando w einen Watchpoint auf die Variable $package, damit das Programm nach einem continue mit c wieder stoppt, falls $package seinen Wert ändert:

  DB<2> w $package
  DB<3> c
  Watchpoint 0:   $package changed:
    old value:  ''
    new value:  'Irgendwie-Sowieso'

Jetzt ist es klar. Für das Modul Irgendwie::Sowieso sucht die Methode parse_abstract() also nach dem regulären Ausdruck /^Irgendwie-Sowieso\s-=s)(.*)/. Der Modulname muss am Zeilenanfang stehen,

    =head1 NAME
 
        Irgendwie::Sowieso - Ein cooles Modul

haut also nicht hin, es muss schon

    =head1 NAME
 
    Irgendwie::Sowieso - Ein cooles Modul

sein, dann wird der Abstract gefunden.

GUI

Wer statt dem etwas nüchternen Kommandozeilen-Interface lieber eine graphische Oberfläche mit Mausbedienung möchte, kann den Data Display Debugger ddd, der jeder Linux-Distribution beiliegt, einfach mit dem Perl-Backend verbinden. Hier ein Aufruf mit dem vorgestellten Script wsrv und http://microsoft.com als Argument:

    ddd -perl wsrv http://microsoft.com

Abbildung 1 zeigt die Oberfläche in Aktion. Nicht nur beim Setzen von Breakpoints ist die graphische Anzeige hilfreich. Auch das konstante Überwachen von Ausdrücken in der ``Display'' Ebene (wie der Skalar $url in Abbildung 1) ist bequem. Ähnliches ließe sich im Perl-Debugger auch mit dem Pre-Prompt-Kommando < erreichen.

    > print("url=$url\n");

legt fest, dass vor jedem neuen Zeilenprompt die angegebene print-Funktion aufgerufen wird, die den aktuellen Wert von $url ausgibt. Ob terminal- oder GUI-basiert: Wie immer nur eine Frage persönlicher Vorliebe. Weitere graphische Varianten sind die kostenpflichtige Komodo-IDE und ptkdb, das sich einfach per CPAN-Shell installieren lässt:

    perl -MCPAN -e 'install(Tk,Devel::ptkdb)'
    perl -d:ptkdb wsrv http://microsoft.com

Abbildung 2 zeigt ptkdb beim Durchschreiten der request-Methode des LWP::UserAgent-Pakets. In der rechten Spalte zeigt die Debugger-Session gerade die einzelnen Elemente des LWP::UserAgent-Objekts an, das der Methode als erstes übergeben wurde.

Tracing

Zum Abschluss noch ein kleiner Trick, um ein Programm jede ausgeführte Zeile anzeigen zu lassen, ohne interaktiv mit dem Debugger einzugreifen. Dieses ``Tracing'' lässt sich über die Environment-Variable PERLDB_OPTS einstellen, bevor der Aufruf des eigentlichen Debuggers erfolgt:

   PERLDB_OPTS="NonStop=1 AutoTrace=1 frame=2" perl -dS program

Die AutoTrace-Option setzt den Debugger in den Tracing-Modus, in dem er jede Source-Zeile erst ausgibt und dann erst ausführt. Mit der NonStop-Option hält der Debugger weder am Anfang noch am Ende an, die Sitzung wird nicht-interaktiv. Mit frame=2 kommen beim Eintritt und beim Verlassen von Unterfunktionen ``entering ...'' und ``exiting ...''-Meldungen hoch, damit der Programmfluss einsichter wird. Wer zusätzlich übergebene Parameter und Rückgabewerte von Unterfunktionen möchte, setzt frame=4. Und schließlich sucht perls Option -S eine ausführbares Perl-Skript program nicht nur im aktuellen Verzeichnis sondern auch in allen in $PATH definierten Pfaden.

Eine kleine Einführung in den Gebrauch des Debuggers findet sich in jeder neuen Perl-Distribution unter perldoc perldebtut. Die ausführliche Dokumentation ist in perldebug und wer in die Innereien des Debuggers vordringen möchte, sollte sich perldebguts zu Gemüte führen. Besonders zu empfehlen ist [2], die ultimative Referenz zum Perl-Debugger im handlichen Taschenformat. Don't leave home without it!

Tabelle

    Dynamische Navigation
    n Nächste Zeile ausführen, danach anhalten.
    s Nächste Zeile starten, in Unterfunktion anhalten.
    r Aktuelle Funktion fertig durchlaufen, dann stopp.
    R Zurück zum Start und nochmal ausführen
    Variablen anzeigen
    p Wert ausgeben
    x Dump (x \%hash)
    Source-Navigation
    l Vorwärts blättern
    - Rückwärts blättern
    v Code um aktuelle Zeile herum zeigen
    . Zurück zur aktuellen Zeile
    f In eine andere Source-Datei wechseln
    Erweiterte dynamische Navigation
    c zeile            Code bis Zeile zeile ausführen, dann stopp.
    c funktion         Code bis Funktion funktion ausführen, in ihr anhalten
    
    b zeile            Breakpoint in Zeile setzen
    b funktion         Breakpoint in Funktion setzen
    b zei/fu condition Breakpoint mit Bedingung
    
    a zei/fu action    Actionpoint in Zeile/Funktion
    w zei/fu variable  Watchpoint in Zeile/Funktion
    > command          Post-Prompt setzen
    L                  Breakpoints, Watchpoints, Actions anzeigen
    B/A/W              Breakpoints, Watchpoints, Actions löschen

Abbildung 1: Die graphische Oberfläche des Multitalents "Data Display Debugger" (ddd) integriert sich mit dem Perl-Debugger.

Abbildung 2: Der auf perl/Tk basierende graphische Debugger-Frontend ptkdb lässt sich leicht vom CPAN installieren.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2005/03/Perl

[2]
``Perl Debugger Pocket Reference'', Richard Foley, O'Reilly 2004

[3]
``Retire your Debugger, log smartly with Log::Log4perl'', Michael Schilli, http://www.perl.com/pub/a/2002/09/11/log4perl.html

[4]
``Perl Debugged'', Peter Scott, Ed Wright, Addison Wesley, 2001

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.