(Bildvorschlag: Schiffskapitän spricht ins ``Rohr'')
Es kommt zwar selten vor, dass der Perl-Interpreter perl
so richtig
abstürzt, doch falls es passiert, hilft auch der ausgezeichnete
Perl-Debugger ([2]) nichts mehr. Die Fehlerursache lässt sich jedoch
mit dem GNU-Debugger gdb
auf C-Ebene einkreisen.
Wer in Perl statt in C oder C++ programmiert, nimmt es oft als selbstverständlich hin, wie viel unnütze Arbeit die Hochsprache erspart. Speicher reservieren, Referenzen zählen, auf wildgewordene Pointer aufpassen, Speicher freigeben -- derlei Sisyphusarbeit hält Perls virtuelle Maschine vom Programmierer fern, damit dieser sich auf das Implementieren der Applikation konzentrieren kann.
Doch auch tief unten im Maschinenraum können Fehler auftreten. Es
kommt zwar extrem selten vor, dass ein Bug in einem Perl-Release die
in C implementierte virtuelle Maschine perl
zum Absturz bringt,
doch auch handgeschriebene Perl-Erweiterungen
eines unachtsamen C/C++-Entwicklers können die Ursache
für einen Absturz sein.
Das Skript in Listing crash
führt zum Beispiel mittels einer C-Erweiterung
bewusst einen Absturz des Interpreters mit einem ``Segmentation Fault''
herbei. Es bedient sich hierzu des CPAN-Moduls Inline
, das angehängten
C-Code compiliert und dynamisch in Skripts einbindet. Der C-Code nach
der __END__-Markierung setzt einen Pointer auf die Adresse 0xcba00000; und
lässt dann die C-Funktion strcpy
brutal in diese zumindest
auf einer 32-bit x86 Architektur geschützte
Kernel-Adresse hineinschreiben. Der Prozessor merkt das, löst einen
Interrupt aus und der Linux-Kernel zieht daraufhin dem ausführenden
Programm perl
den Teppich unter den Füßen weg.
01 #!/usr/bin/perl -w 02 use strict; 03 use Inline "C"; 04 use Inline Config => 05 CLEAN_AFTER_BUILD => 0; 06 07 c_crash(43); 08 09 __END__ 10 __C__ 11 int c_crash( int num ) { 12 char *cp = 0xcba00000; 13 strcpy(cp, "Ouch!"); 14 }
Abbildung 1: Eine gdb-Session bringt den Stacktrace zu Tage |
Abbildung 1 zeigt, wie man das Perl-Skript zur Reproduktion des Fehlers
im GNU-Debugger gdb
aufruft. Das ausführende Binärprogramm ist der
Perl-Interpreter perl
, also wird der Debugger mit gdb perl
gestartet.
Um den Interpreter mit dem Perl-Skript crash
zu starten, wird anschließend
im Debugger das Kommando run crash
aufgerufen.
Nach dem Absturz liefert gdb
nicht nur den C-Code der Zeile, die den Crash auslöste.
Das Kommando bt
(für Backtrace, alternativ funktioniert auch where
)
zeigt die aufrufende C-Funktionshierarchie im sogenannten Stacktrace an.
Damit der Debugger ausgeführte Funktionen
den Zeilennummern im C-Source-Code zuordnen kann, sollte perl
mit
dem Compiler-Flag -g
kompiliert werden.
Auf die Frage
What optimizer/debugger flag should be used? des Konfigurationsskripts
Configure
sollte der Admin -g
antworten oder Configure gleich
mit
./Configure -D optimize=-g -d
aufrufen. Wurde dies versäumt, ist die Analyse mangels Referenzen zum C-Source-Code schwieriger, und ist gar das Executable ``gestrippt'', sieht's ganz düster aus, denn aus deassimblierten Assemblercode schlau zu werden, bleibt langbärtigen Gurus vorbehalten.
Aber auch aus einem
'normal' kompilierten perl
lassen sich Informationen herausholen.
Die Autopsie gestaltet sich dann schwieriger, doch ein später
erläuterter Trick hilft dabei, exzessives Jonglieren mit Hex-Zahlen
zu vermeiden.
Tritt ein Crash-Problem in einem laufenden Programm auf,
erzeugt der Linux-Kernel
normalerweise eine core
-Datei. Ist dies nicht der Fall, unterdrückt
die bash
-Shell wahrscheinlich die core
-Produktion mit der
Standard-Einstellung ulimit -c 0
. Setzt man hingegen
ulimit -c unlimited
, dann entsteht eine core-Datei
core
(oder auch core.xxxx
mit angehängter Prozess-ID):
$ ./crash Segmentation fault (core dumped) $ ls -l core.* -rw------- 1 mschilli mschilli 1658880 Nov 3 21:30 core.1234
Sie liegt normalerweise im aufrufenden Verzeichnis,
es sei denn, in /proc/sys/kernel/core_pattern
ist etwas anderes definiert.
Um herauszufinden, was den Crash ausgelöst hat, ruft man
den Debugger post mortem mit dem ausführenden Programm und dem Core-File
auf (z.B. gdb perl core.1234
). Man erhält eine ähnliche Debugger-Session
wie in Abbildung 1, aus der sich ebenfalls der Stacktrace kurz vor dem
Absturz ermitteln lässt. Starten lässt sich so ein Speicher-Schnappschuss
allerdings nicht mehr.
Anhand des Stacktrace von Abbildung 1 lässt sich ablesen, dass perl
in der Datei crash_3e35.xs
(Zeile 7) in der C-Funktion c_crash()
bei dem Versuch abgestürzt ist,
die C-Funktion strcpy()
auszuführen. Kontrolliert man mit dem
Debugger-Kommando print cp
die
Zieladresse, kommt 0xcba00000
heraus, was dem untersuchenden
Kriminologen den Absturz erklärt.
Doch gdb
gibt nur Aufschluss über die Vorgänge auf C-Ebene.
Wie kann man aber herausfinden, in welchem Perlskript und in welcher
Perlzeile der Absturz erfolgte? Hierzu muss man Perls C-Datenstrukturen
analysieren, die über den Zustand der virtuellen Maschine zum
Absturzzeitpunkt Aufschluss geben.
Wie lässt sich herausfinden, dass c_crash mit dem Argument 43
aufgerufen wurde? Dazu ist die Kenntnis einiger Perl-Interna
notwendig, die in den Manualseiten perlguts
und perlhack
stehen.
Perls virtuelle Maschine legt ähnlich wie
ein C-Compiler Funktionsargumente auf einem Stack ab, bevor eine
Perl-Funktion aufgerufen wird. Auf die Spitze des Argumentenstacks zeigt
die Variable PL_stack_sp
,
und dort findet sich eine von Perls SV-(Scalar Value)-Strukturen.
Um den Integerwert herauszufieseln, muss PL_stack_sp->sv_any
erst
auf (XPVIV*)
gecastet werden, dann liefert dessen xiv_iv
-Eintrag
den Zahlenwert des c_crash()
übergebenen Arguments:
(gdb) p ((XPVIV*) PL_stack_sp->sv_any)->xiv_iv $1 = 43
Der Debugger gdb
kann sich auch in einen laufenden Prozess einhängen.
Der Prozess wird dann automatisch kurz angehalten und im Debugger
auf Geheiß der Benutzers schrittweise weitergeschubst.
Dies ist besonders dann hilfreich, wenn ein Perl-Prozess 'hängt',
also keine Fortschritte zeigt und kein Logging implementiert wurde.
Wo hängt's?
Das Perl-Programm in Listing spinner
ruft nur eine mit
sleep()
gebremste Endloschleife auf
und gibt seine Prozessnummer und die aktuelle Uhrzeit in Sekunden
nach 1970 aus. Wird die Prozessnummer zum Beispiel mit 1234 angezeigt,
dockt der Aufruf
gdb perl -p 1234
an den laufenden Prozess an. Statt des Kommandozeilen-Debuggers kommt
diesmal der graphische Debugger ddd
zum Einsatz, der in Abbildung
2 zu sehen ist und guten Linux-Distributionen normalerweise beiliegt.
Er versteht die Kommandozeilenoptionen des gdb
, also ist beim
Aufruf oben lediglich gdb
durch ddd
zu ersetzten.
Mit großer Wahrscheinlichkeit erwischt man den Perl-Prozess
im mit sleep()
eingeleiteten Sekundenschlaf.
01 #!/usr/bin/perl -w 02 use strict; 03 04 while(1) { 05 function(time); 06 sleep(1); 07 } 08 09 sub function { 10 my($time) = @_; 11 12 print "$$: $time\n"; 13 }
Abbildung 2: Der GUI-Debugger ddd hat sich an einen Perl-Prozess angedockt und gibt die Opcodes aus, die Perls virtuelle Maschine gerade durchläuft. |
Das Kommando up
lässt den Debugger in höhere Stackframes hüpfen,
also nach oben in der Hierarchie aufrufender Funktion springen.
Vier
Ebenen weiter zeigt das Source-Code-Fenster die große while
-Schleife,
in der der Perl-Interpreter die Opcodes eines Skripts auf
der virtuellen Maschine abarbeitet (Abbildung 2).
Diese Opcode-Strukturen sind die Bausteine, aus denen Perlprogramme
bestehen, nachdem der Compiler den Sourcecode eines Skripts
übersetzt hat.
Von welchem Typ ist nun die dort sichtbare globale Variable
PL_op
? Das ist beim
exzessiven Gebrauch von Macros im Perl-Kern manchmal gar nicht so
leicht zu ermitteln. gdb
weiß es aber:
(gdb) whatis PL_op type = OP *
Der Befehl print *PL_op
im unteren gdb
-Fenster
zeigt den Inhalt der Datenstruktur
an. PL_op
ist ein Pointer auf eine Struktur, der Stern *
weist
gdb
an, nicht die Adresse, sondern den Inhalt der Datenstruktur
anzuzeigen. Um die Opcode-Daten wie in Abbildung 2 graphisch im oberen
Fenster des ddd
dauerhaft darzustellen, gibt man
graph display `p PL_op`
ins gdb
-Fenster ein und führt anschließend einen Doppelklick
auf das blau unterlegte Hex-Adresse des im oberen Display erscheinenden
Opcode-Kastens aus. Daraufhin expandiert ddd
die hinter der Adresse
lungernde Datenstruktur und stellt ihre Attribute in dem neuen,
größeren Kasten rechts davon dar.
Die Ausgabe zeigt, dass der OP-Knoten nicht nur Zeiger
auf nachfolgende-OPs und eine Adresse für den auszuführenden Code
beherbergt, sondern auch ein Feld op_type
, das den Typ des
Opcodes angibt.
Um den Durchlauf der Opcodes bei einem laufenden Perlprogramm zu zeigen, werden folgende Aktionen für den in Abbildung 2 rot eingezeichneten Breakpoint definiert:
commands 1 silent p PL_op->op_ppaddr cont end
Der Debugger soll jedesmal an diesem ersten (deswegen die 1)
Breakpoint anhalten, aber keine Zeilen/Code-Information ausspucken
(silent
), sondern Adresse und Name der Funktion, die den
Opcode implementiert (PL_op->op_ppaddr
). Der anschließende
cont
-Befehl bestimmt, dass gdb
sofort im Opcode-Reigen fortfahren soll,
ohne auf Benutzereingaben zu warten.
Das untere Kommandofenster in Abbildung 2 zeigt die Ausgaben, nachdem
der Prozess nach der Breakpointdefinition wieder mit cont
fortgesetzt
wurde.
Für die Laufzeitanalyse wildgewordener Perlprogramme eignen sich
besonders Opcodes vom Typ nextstate
.
Sie geben Hinweise darauf, in welchem Perl-Paket und an welcher
Zeile der Original-Perl-Code zu finden ist, den die virtuelle
Maschine gerade ausführt.
nextstate
-Opcodes führen die Typ-Nummer 174, also wird flugs der
alte Breakpoint mit delete 1
gelöscht und
ein neuer Breakpoint, ebenfalls am Ende der while
-Schleife gesetzt:
(gdb) break if PL_op->op_type == 174 (gdb) display Perl_op_dump(PL_op)
Die nach break
folgende if
-Bedingung
definiert, dass gdb
nur dann anhält, falls ein Opcode des Typs 174
abgearbeitet wird. Der Befehl display
bestimmt eine Aktion, die
gdb
nach jedem Anhalten ausführt. Er eignet sich besonders
zur Ausgabe von Variablenwerten.
Die C-Funktion Perl_op_dump()
nimmt die perl
-interne
Datenstruktur eines Opcode entgegen und gibt dessen Attribute
per printf
-Anweisung aus. Sie stammt aus dem Perl-Interpreter
und dient den Perl-Kernentwicklern zum Debuggen wackeliger
Development-Versionen.
gdb
führt zu Analysezwecken problemlos Funktionen aus, die irgendwo
im gerade untersuchten Executable oder dessen Libraries definiert sind,
also bietet es sich an, vorgefertigte Funktionen zu verwenden.
Auch in einem Perl, das ohne -g
compiliert wurde, also keine
Debuginformationen enthält, lässt sich die gegenwärtig ablaufende
Perlcodezeile ermitteln. gdb
weiß in diesem Fall jedoch nicht mehr,
dass die Variable PL_op
vom Typ OP
ist und ein Attribut
op_type
besitzt. Man könnte den Offset von op_type
zum
Strukturanfang PL_op
ausrechnen, und mit Hilfe der bekannten
Endian-ness des Intel-Prozessors (das niederwertige Byte kommt zuerst)
den Wert des Attributs op_type
ermitteln und ihn mit 174
vergleichen.
Einfacher geht es mit einem Trick: Wir stellen uns eine kleine
shared Library nach Listing optest.c
her und übersetzen sie
mit Hilfe des Perl-Skripts perl_compile
. perl
speichert
ja die Compileroptionen und die Parameter, mit denen es
konfiguriert wurde und stellt sie über das Modul Config
und
den Hash %Config
zur Verfügung. Kompiliert man eine Perl-Erweiterung,
lassen sich so schnell die richtigen Compile-Optionen und
Include-Pfade einstellen.
1 #include "EXTERN.h" 2 #include "perl.h" 3 #include "XSUB.h" 4 5 struct op **my_special_op = NULL;
01 #!/usr/bin/perl -w 02 use strict; 03 use Config; 04 05 my($file) = @ARGV; 06 die "usage $0 file.c" unless defined $file; 07 (my $solib = $file) =~ s/\.c/.so/; 08 09 my $cmd = "gcc -shared -o $solib " . 10 "$Config{ccflags} -g -fpic " . 11 "-I$Config{archlibexp}/CORE $file"; 12 13 system $cmd;
Bei diesem Prozess kommt eine shared Library namens optest.so
heraus, die einzig eine globale Pointer-Variable my_special_op
vom
Typ OP **
enthält, die später auf die Adresse des
Opcode-Pointers PL_op
eingestellt wird, der vom Typ OP *
ist.
Da die shared Lib mit -g
kompiliert wurde, kennt gdb
die
Datenstruktur von my_special_op
und erlaubt es so indirekt,
den Wert von PL_op->op_type
zu erfragen.
Damit dies läuft, wird die Testlibrary mit LD_PRELOAD vor dem eigentlichen
Executable geladen, wie aus Abbildung 3 ersichtlich. Nach der
Definition des Breakpoints mit der bekannten Bedingung und
dem Ausgabekommando zeigt die Ausgabe, dass Perl nach der
Unterbrechnung zunächst die Zeile 10, dann die Zeile 12 des Hauptprogramms
abarbeitet. Ein Nachzählen der Zeilen ergibt, dass es sich um die
erste und die zweite Codezeile in function()
in listing spinner
handelt.
Zu beachten ist, dass der nextstate
-Opcode nicht direkt
den Dateinamen des gerade abgearbeiteten Perlskripts angibt. Statt
dessen liefert es den Namen des gerade aktiven Perl-Pakets. Ist
dies "main"
, befindet sich der Interpreter im Hauptprogramm. Ist
er z.B. "LWP::UserAgent"
, lässt sich normalerweise
recht schnell mit perldoc -m LWP::UserAgent
herausfinden, welche Datei mit
das Paket definiert.
Abbildung 3: Die gerade abgearbeitete Zeile eines gerade laufenden Perlskripts ohne Debuginformation wird ermittelt. |
Ist der Fehler so eingekreist, ist die Behebung meist trivial. Und um beim nächsten Problem die Analyse zu erleichtern, hilft eine sorgfältig umgesetzte Logging-Strategie.
Zum vertieften Studium der vorgestellten und zahlreicher weiterer Analysetechniken unter Linux sei [3] empfohlen, ein einzigartiges Werk, das auf keinem Programmiererschreibtisch fehlen sollte.
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. |