Dreht sich das Perl-Rad nur noch knirschend, sind vielleicht Fehler im Perl-Interpreter oder in Erweiterungsmodulen schuld. Der gute alte GDB inspiziert das Getriebe und kommt Problemstellen auf die Schliche.
Wer in Perl statt in C oder C++ programmiert, nimmt es oft als selbstverständlich hin, dass ihm viel unnütze Arbeit erspart bleibt: Speicher reservieren und freigeben, Referenzen zählen, auf wild gewordene Pointer aufpassen – derlei Sisyphusarbeit hält Perls virtuelle Maschine vom Programmierer fern, damit der sich auf das Implementieren konzentrieren kann.
Doch auch tief unten im Maschinenraum können Fehler auftreten. Es kommt zwar sehr selten vor, dass ein Bug in einer Perl-Release die in C implementierte virtuelle Maschine zum Absturz bringt. Häufiger aber können handgeschriebene Perl-Erweiterungen eines unachtsamen C/C++-Entwicklers dafür eine mögliche Ursache sein.
Nur in Ausnahmefällen stürzt der Perl-Interpreter »perl« so richtig ab, wenn es aber doch passiert, hilft auch der Perl-Debugger [2] nicht mehr weiter. Das Skript in Listing 1 führt zum Beispiel mit Hilfe einer C-Erweiterung bewusst einen Absturz des Interpreters mit einem Segmentation Fault herbei.
|
Listing 1: |
|---|
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 }
|
Linux zieht den Teppich weg
Das Beispiel bedient sich hierzu des CPAN-Moduls Inline, das angehängten C-Code kompiliert und dynamisch in das Skript einbindet. Der C-Code nach der »__END__«-Markierung setzt einen Pointer auf die Adresse »0xcba00000« und lässt dann die C-Funktion »strcpy« rücksichtslos an dieser, zumindest in der 32-Bit-x86-Architektur geschützten Kerneladresse schreiben. Der Prozessor merkt das, löst einen Interrupt aus und der Linux-Kernel zieht daraufhin dem schuldigen Programm den Teppich unter den Füßen weg.
Abbildung 1 zeigt, wie das Perl-Skript für die Reproduktion des Fehlers im Gnu-Debugger »gdb« aufzurufen ist. Das ausführende Binärprogramm ist der Perl-Interpreter, also startet der Debugger mit »gdb perl«. Um dann das Perl-Skript »crash« vom Interpreter abarbeiten zu lassen, ruft man im Debugger das Kommando »run crash« auf. Nach dem Absturz liefert »gdb« nicht nur den C-Code jener Zeile, die den Crash auslöste. Das Debugger-Kommando »bt« (für Backtrace, alternativ funktioniert auch »where«) zeigt zusätzlich die aufrufende C-Funktionshierarchie im so genannten Stacktrace an.
Damit der Debugger ausgeführte Funktionen den Zeilennummern im C-Sourcecode zuordnen kann, muss der Fehlersucher sein Perl zuvor mit dem Compiler-Flag »-g« kompiliert haben. Auf die Frage des Konfigurationsskripts: »What optimizer/debugger flag should be used?« sollte er dazu »-g« antworten oder das Skript gleich mit »./Configure -D optimize=-g -d« aufrufen.
Versäumt er dies, ist die Analyse mangels Referenzen zum C-Sourcecode schwieriger, und wenn das Executable auch noch gestrippt ist, sieht es ganz düster aus, denn disassemblierten Assemblercode verstehen bleibt langbärtigen Gurus vorbehalten. Aber auch aus einem normal kompilierten »perl« lassen sich Informationen herausholen. Die Autopsie gestaltet sich dann zwar schwieriger, doch ein Trick, dessen Erklärung weiter unten folgt, hilft dabei, exzessives Jonglieren mit Hex-Zahlen zu vermeiden.
Obduktion der Skriptleiche
Kommt es in einem laufenden Programm zum Crash, erzeugt der Linux-Kernel normalerweise eine Core-Datei. Ist dies nicht der Fall, unterdrückt die Bash wahrscheinlich die Core-Produktion mit der Standardeinstellung »ulimit -c 0«. Lautet die Einstellung hingegen »ulimit -c unlimited«, dann entsteht eine Core-Datei (»core« oder auch »core.PID« mit angehängter Prozess-ID):
$ ./crash Segmentation fault (core dumped) $ ls -l core.* -rw------- 1 mschilli mschilli 1658880 Nov 3 21:30 core.1234
Der so genannte Core-Dump liegt normalerweise in dem aufrufenden Verzeichnis, es sei denn, in »/proc/sys/kernel/core_pattern« ist etwas anderes definiert. Wer herausfinden will, was den Crash ausgelöst hat, ruft den Debugger post mortem mit dem ausführenden Programm und dem Core-File auf (beispielsweise »gdb perl core.1234«). Er erhält eine ähnliche Debugger-Session, wie sie in Abbildung 1 zu sehen ist. Aus ihr lässt sich ebenfalls der Stacktrace kurz vor dem Absturz ermitteln. Zu starten ist ein solcher Speicher-Schnappschuss allerdings nicht mehr.
Perlentaucher
Anhand des Stacktrace von Abbildung 1 lässt sich ablesen, dass »perl« in der Datei »crash_3e35.xs« in der C-Funktion »c_crash()« bei dem Versuch abgestürzt ist, die C-Funktion »strcpy()« auszuführen. Wer mit dem Debugger-Kommando »print cp« die Zieladresse kontrolliert, erhält als Ergebnis »0xcba00000«, was dem untersuchenden Kriminologen den Absturz erklärt.
Der Debugger »gdb« gibt nur Aufschluss über die Vorgänge auf C-Ebene. Um zusätzlich herausfinden, in welchem Perl-Skript und in welcher Perl-Zeile der Absturz erfolgte, muss die Untersuchung auch Perls C-Datenstrukturen einbeziehen. Sie geben nämlich über den Zustand der virtuellen Maschine zum Absturzzeitpunkt Aufschluss.
Wie lässt sich herausfinden, dass »c_crash« mit dem Argument »43« aufgerufen wurde? Dazu ist die Kenntnis einiger Perl-Interna notwendig, die sich in den Manualseiten »perlguts« und »perlhack« nachlesen lassen. Perls virtuelle Maschine legt ähnlich wie ein C-Compiler Funktionsargumente auf einem Stack ab, bevor sie eine Perl-Funktion aufruft. Auf die Spitze des Argumente-Stack zeigt die Variable »PL_stack_sp«, und genau dort findet sich eine von Perls SV-Strukturen (Scalar Value).
Um den Integerwert herauszufieseln, muss »PL_stack_sp->sv_any« erst auf »(XPVIV*)« gecastet werden, dann liefert dessen »xiv_iv«-Eintrag den Zahlenwert des an »c_crash()« übergebenen Arguments:
(gdb) p ((XPVIV*) PL_stack_sp->sv_any)->xiv_iv $1 = 43
Der Debugger »gdb« ist auch dazu in der Lage, sich in einen laufenden Prozess einzuhängen. In diesem Fall hält er den Prozess kurz an und schubst ihn auf Geheiß der Benutzers schrittweise weiter. Das ist besonders dann hilfreich, wenn ein Perl-Prozess hängt, also keine Fortschritte zeigt und kein Logging implementiert wurde.
Ozapft is!
Das Perl-Programm in Listing 2 ruft nur eine mit »sleep()« gebremste Endlosschleife auf und gibt seine Prozessnummer sowie 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 grafische Debugger »ddd« zum Einsatz, der in Abbildung 2 zu sehen ist und guten Linux-Distributionen normalerweise beiliegt.
|
Listing 2: |
|---|
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 "$$: $timen";
13 }
|

Abbildung 2: Der GUI-Debugger »ddd« hat sich an einen Perl-Prozess angedockt und gibt die Opcodes aus, die die virtuelle Maschine durchläuft.
Er versteht die Kommandozeilenoptionen des »gdb«, also ist beim Aufruf im Beispiel lediglich »gdb« durch »ddd« zu ersetzen. Mit großer Wahrscheinlichkeit liegt der Perl-Prozess gerade in seinem mit »sleep()« eingeleiteten Sekundenschlaf. Das Kommando »up« – im Konsolenfenster des Debuggers eingegeben – lässt ihn in höhere Stackframes hüpfen, also nach oben in der Hierarchie aufrufender Funktion.
Vier Ebenen weiter zeigt das Sourcecode-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 Perl-Programme bestehen, nachdem der Just-in-Time-Compiler den Sourcecode eines Skripts übersetzt hat.
Von welchem Typ die dort sichtbare globale Variable »PL_op« ist, lässt sich bei dem exzessiven Gebrauch von Makros 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« dazu an, nicht die Adresse, sondern den Inhalt der Datenstruktur anzuzeigen. Um die Opcode-Daten wie in Abbildung 2 grafisch im oberen Fenster des »ddd« dauerhaft darzustellen, ist »graph display `p PL_op`« ins »gdb«-Fenster einzutragen und anschließend ein Doppelklick auf die blau unterlegte Hex-Adresse des im oberen Display erscheinenden Opcode-Kastens auszuführen.
Daraufhin expandiert »ddd« die hinter der Adresse verborgene 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 enthält, sondern außerdem noch ein Feld »op_type«, das den Typ des Opcode angibt.
Parade der Opcodes
Um den Durchlauf der Opcodes bei einem laufenden Perl-Programm zu zeigen, definiert der Anwender folgende Aktionen für den in Abbildung 2 rot eingezeichneten Breakpoint:
commands 1 silent p PL_op->op_ppaddr cont end
Der Debugger soll jedes Mal an diesem ersten (daher die »1«) Breakpoint anhalten, aber keine Zeilen- oder Code-Information ausspucken (»silent«), sondern Adresse und Namen 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 Breakpoint-Definition mit »cont« wieder in Gang kam.
Codeknacker
Für die Laufzeitanalyse wild gewordener Perl-Programme 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.
Die »nextstate«-Opcodes führen die Typnummer 174. Also flugs den alten Breakpoint mit »delete 1« löschen und einen neuen setzen, ebenfalls am Ende der »while«-Schleife:
(gdb) break if PL_op->op_type == 174 (gdb) display Perl_op_dump(PL_op)
Die auf »break« folgende »if«-Bedingung definiert, dass »gdb« nur dann anhält, falls Perl einen Opcode des Typs 174 abarbeitet. 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 wackliger Development-Versionen. GDB führt zu Analysezwecken problemlos Funktionen aus, die irgendwo im gerade untersuchten Executable oder dessen Libraries definiert sind.
Verschärfte Suche
Auch wenn »perl« ohne »-g« kompiliert wurde, also keine Debug-Informationen enthält, lässt sich die gerade ablaufende Perl-Codezeile ermitteln. GDB weiß in diesem Fall aber nicht mehr, dass die Variable »PL_op« vom Typ »OP« ist und ein Attribut »op_type« besitzt. Der Programmierer könnte zwar den Offset von »op_type« am Strukturanfang »PL_op« ausrechnen und mit der bekannten Endianess des Intel-Prozessors (das niederwertige Byte kommt zuerst) den Wert des Attributs »op_type« ermitteln und ihn mit »174« vergleichen.
Tricksen mit Bibliotheken
Einfacher macht er es sich aber mit einem Trick: Er stellt sich eine kleine Shared Library nach dem Muster von Listing 3 her und übersetzen sie mit Hilfe des Perl-Skripts »perl_compile« (Listing 4). Perl speichert ja die Compiler-Optionen und die Parameter, mit denen es konfiguriert wurde, und stellt sie über das Modul »Config« und den Hash »%Config« zur Verfügung. Beim Kompilieren einer Perl-Erweiterung, lassen sich so schnell die richtigen Compile-Optionen und Include-Pfade einstellen.
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 die Adresse des Opcode-Pointers »PL_op« enthält, 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 wie geplant funktioniert, erfolgt das Laden der Testlibrary mit »LD_PRELOAD« vor dem des eigentlichen Executable, wie Abbildung 3 zeigt. Nach der Definition des Breakpoints – deren Bedingung jetzt dem kleinen Trick mit der Library anzupassen ist – und dem Ausgabekommando zeigt der Output, dass Perl nach der Unterbrechung 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 2 handelt.
|
Listing 3: |
|---|
01 #include "EXTERN.h" 02 #include "perl.h" 03 #include "XSUB.h" 04 struct op **my_special_op = NULL; |
|
Listing 4: |
|---|
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;
|

Abbildung 3: Beispielsitzung mit dem »gdb« und einer trickreichen Shared Library: Die gerade abgearbeitete Zeile eines aktuell ablaufenden Perl-Skripts ohne Debug-Information wird ermittelt.
Loggen löst Probleme
Zu beachten ist noch, dass der »nextstate«-Opcode nicht direkt den Dateinamen des gerade abgearbeiteten Perl-Skripts angibt. Stattdessen liefert es den Namen des gerade aktiven Perl-Pakets. Ist dies »”main”«, befindet sich der Interpreter im Hauptprogramm. Lautet der Name beispielsweise »”LWP::UserAgent”«, lässt sich normalerweise recht schnell mit »perldoc -m LWP::UserAgent« herausfinden, welche Datei genau das Paket definiert.
Ist der Fehler so eingekreist, ist die Behebung meist trivial, und um beim nächsten Problem die Analyse zu erleichtern, hilft eine gute Logging-Strategie. Zum Studium der vorgestellten und zahlreicher weiterer Analysetechniken unter Linux sei [3] empfohlen, ein einzigartiges Werk, das auf keinem Programmiererschreibtisch fehlen sollte. (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2007/01/Perl] [2] Michael Schilli, “Humpeln zur Diagnose”: Tutorial für den Perl-Debugger, Linux-Magazin 04/05 [3] Mark Wilding and Dan Behman, “Self-Service Linux”: Prentice Hall, 2006 |
|
Der Autor |
|---|
|
|








