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.
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.
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";
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.
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.
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.
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 perl
s 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!
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. |
Michael Schilliarbeitet 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. |