Hallo, Maschinenraum? (Linux-Magazin-News, Januar 2007)

(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.

Linux zieht den Teppich weg

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.

Listing 1: crash

    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.

Obduktion am toten Skript

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

Ozapft is!

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.

Listing 2: spinner

    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.

Parade der Opcodes

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.

Verschärfte Suche

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.

Listing 3: optest.c

    1 #include "EXTERN.h"
    2 #include "perl.h"
    3 #include "XSUB.h"
    4 
    5 struct op **my_special_op = NULL;

Listing 4: perl_compile

    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.

Infos

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

[2]
``Humpeln zur Diagnose'', Tutorial für den Perl-Debugger, Michael Schilli, Linux-Magazin 04/2005

[3]
``Self-Service Linux'', Mark Wilding and Dan Behman, Prentice Hall, 2006 Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2007/01/Perl

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.