Segmentation Faults erfolgreich debuggen

Segmentation Faults – Der schlimmste aller Fehler ist …
Kommentare

Auch Softwareentwickler machen Fehler. Wenn sich diese Fehler in einer PHP-Applikation verstecken und auch noch die eigenen sind, hat man beim Beheben quasi einen Heimvorteil. Befinden sich die Fehler in der eigenen Domäne, kann man sie mit Standardwerkzeugen ausbessern. Doch es gibt auch Fehler, die sich weder im eigenen Code noch in der vertrauten Programmiersprache befinden: die Segmentation Faults. Sie verhalten sich nicht wie herkömmliche Bugs und müssen anders behandelt werden. Welche Werkzeuge zur Behebung der Segmentation Faults zur Verfügung stehen, soll folgender Artikel zeigen.

Der Begriff „Segmentation Fault“ ist heute als Bezeichnung für eine Speicherschutzverletzung nicht mehr korrekt, da die meisten Betriebssysteme Paging zur Speicherverwaltung einsetzen. Trotzdem hat sich dieser Begriff erhalten. Eine Speicherschutzverletzung eines Programms liegt dann vor, wenn es versucht, auf eine Speicherregion schreibend zuzugreifen, die entweder schreibgeschützt oder von einem anderen Programm reserviert ist. Weitere häufige Auslöser einer Schutzverletzung sind folgende:

  • Nutzung von uninitialisierten Pointern, die auf keine feste Adresse zeigen
  • Nutzung von Pointern, die auf schon freigegebene Speicherbereiche verweisen
  • ein Hardwaredefekt

Der Kernel löst bei einer Speicherschutzverletzung durch ein Programm ein SIGSEGV-Signal aus und teilt so dem aufrufenden Programm mit, dass es eine Schutzverletzung begangen hat. Das Programm reagiert sofort mit einem Ausführungsstopp und schreibt vorher, falls konfiguriert, noch ein Abbild seines Speicherzustands in einem speziellen Format in ein vorher konfiguriertes Verzeichnis. Dieses Abbild ist der Core Dump, der den Speicherzustand des Programms zum Zeitpunkt des Ausführungsstopps beinhaltet.

Bei PHP-Programmen beendet der Interpreter sofort die Programmausführung und zeigt eine Nachricht im konfigurierten Error Log oder an der Kommandozeile an. Vom PHP-Interpreter ausgelöste Segmentation Faults können nicht direkt von einem PHP-Skript stammen. In einer Skriptsprache wie PHP beschreibt das Skript dem Interpreter, welche Funktion er auf der unterliegenden Ebene (bei PHP C-Code) ausführen soll. Die hinter der Beschreibung liegenden Anweisungen werden dann im C-Code auf der Maschine ausgeführt. Ein Segmentation Fault wird daher immer von einer PHP Extension oder dem PHP Core in den unterliegenden C-Funktionen ausgelöst. PHP-Code kann also nicht eigenständig den Segmentation Fault erzeugen. Daher kann ein Fehler auch nicht direkt mit PHP-Werkzeugen gefunden, allerdings auf der PHP-Skript-Ebene unter Nutzung verschiedener Techniken eingegrenzt und gegebenenfalls über Workarounds vermieden werden. Die Fehlerbehebung ist dann aber nur innerhalb der C-Funktionen möglich. Das bedeutet, dass PHP oder seine Extension neu kompiliert werden muss.

Wie zeigt sich ein Segmentation Fault

Der Segmentation Fault führt immer zu einem sofortigen Abbruch der Programmausführung. Wenn PHP nur auf der Kommandozeile ausgeführt wird, ist sehr einfach zu erkennen, dass ein Segmentation Fault vorliegt. Der Interpreter meldet diesen Fehler einfach über stderr an die Konsole, in der der Segmentation Fault geschrieben wird.

Listing 1

message = $message;
}

function serialize()
{
return serialize(array($this->message, $this->member));
}

function unserialize($serialized)
{
list($this->message, $this->member) = unserialize($serialized);
}
}
	
$constructed = new Test("original");
$constructed->member = $constructed; //References to self (in this example pointless, but technically legal)
var_dump($constructed);
$transported = unserialize(serialize($constructed)); // Segmentation fault.

Listing 2

object(Test)#1 (2) {
["member"]=>
*RECURSION*
["message"]=>
string(8) "original"
}
Segmentation fault

Etwas schwieriger ist es, mit PHP-Skripten, die über einen Webserver ausgeführt werden, einen Segmentation Fault zu erkennen. Es muss unterschieden werden, ob PHP direkt als Modul des Webservers (z. B. mod_php) oder als Fast-CGI (php-cgi in Debian Squeeze) ausgeführt wird. Bei Verwendung von Nginx und php-cgi und der Anfrage nach reproduceSegFault.php wird einfach eine HTTP-Fehlermeldung ausgegeben: 502 Bad Gateway (Listing 1, der Quelltext ist bei bugs.php.net zu finden und ermöglicht in der Version PHP 5.3.3 das Produzieren eines Segmentation Faults). Dieser Aufruf zeigt, dass der Interpreter die Verarbeitung gestoppt hat und der Nginx-Webserver nicht mehr mit der php-cgi-Instanz kommunizieren kann.

Der Aufruf des Skripts über das Apache-mod_php-Modul führt zur Ausgabe des Vardumps (Listing 2, ausgeführt mit PHP an der Kommandozeile). Da kein Output Buffering (Ausgabezwischenspeicherung) genutzt wird, wird die Ausgabe noch an den Browser gesendet, bevor das Skript mit dem Segmentation Fault abbricht. Man kann dieses Verhalten einfach beobachten, indem man vor und nach der mit „Segmentation Fault“ kommentierten Zeile eine Ausgabe einfügt:

<?php
....
echo "Hallo Welt 1 !";
$transported = unserialize(serialize($constructed)); // Segmentation fault.
echo "Hallo Welt 2 !";

Bei der Ausführung über mod_php wird „Hallo Welt 1 !“ noch ausgegeben, „Hallo Welt 2 !“ aber nicht mehr, da vorher schon die Programmausführung mit einem Segmentation Fault abgebrochen wurde. Bei der Verwendung des Output Bufferings ist die leere oder „weiße“ Seite als Antwort auf eine Anfrage ein Indiz für das Auftreten eines Segmentation Faults. Viele Frameworks verwenden das Output Buffering, um damit zum Beispiel HTTP Header erst am Ende der Ausführung gesammelt auszugeben. Wenn die Ausgabe der zwischengespeicherten Daten nicht mehr erfolgen kann, weil zum Beispiel ein Segmentation Fault ausgelöst wurde, wird nur eine leere Antwort an den Anfrager (Browser) zurückgesendet. Wird dem vorherigen Beispiel die Anweisung zum Output Buffering hinzugefügt, zeigt es nach der Ausführung die Listing 3 dargestellte Ausgabe im Browser, und zwar eine leere Seite.

Listing 3

<?php
...
ob_start();
echo "Hallo Welt 1 !";
$output = ob_get_content();
ob_end_clean();
$transported = unserialize(serialize($constructed)); // Segmentation fault.
echo $output;

Im Livebetrieb einer Webapplikation sind Segmentation-Fault-Meldungen je nach verwendeter Konfiguration im System-, Webserver- oder einem eigenen PHP-Log zu finden. Eine zeitnahe automatische Auswertung und Aggregation der Log-Dateien hilft, die Auslöser der Fehler zu lokalisieren und sie zu beheben. Es stehen verschiedene Hilfsmittel und Techniken zur Verfügung, um einen schnellen Einblick in die Log-Dateien zu bekommen und automatisiert auf Fehler zu reagieren. Sinnvoll kann zum Beispiel das Sammeln der Logs in einer Datenbank sein, um mit einem Werkzeug wie Papertrail Fehler aggregiert zu sehen. Eine kontinuierliche Überwachung der für das Business wichtigsten Seiten der Applikation mit einem Werkzeug wie LiveTest sichert auch bei einem Update von PHP oder der PHP Extensions die fehlerfreie Auslieferung der Webseiten.

Debugging eines Segmentation Faults

Da ein Segmentation Fault nur von PHP Extensions oder dem PHP Core erzeugt werden kann, ist ein PHP-seitiges Debugging nur bis zu einem gewissen Punkt sinnvoll und möglich. Dennoch kann schon eine einfache echo-Anweisung helfen, den Fehler zu lokalisieren. Listing 3 zeigt genau so eine Eingrenzung, bei der ober- und unterhalb der im Verdacht der Fehlerauslösung stehenden Zeile eine echo-Anweisung eingefügt wird. Mit vielen aufeinanderfolgenden echo-Ausgaben ist die Eingrenzung des Fehlers auf eine Zeile möglich. Da diese Art des „Debuggens“ unübersichtlich und langsam ist, empfiehlt sie sich nur bei sehr kleinen und überschaubaren Projekten. Besser geeignet zur Fehlereingrenzung ist ein PHP-Debugger wie Xdebug der Zend_Debug. Er ermöglicht in Verbindung mit einer entsprechend konfigurierten IDE (Eclipse PDT, Zend Studio, PHPStorm etc.) das Setzen von Breakpoints und ein Step-through der Applikation. Dadurch lassen sich relativ schnell Aussagen über den wahrscheinlichen Auslöser des Fehlers treffen.

Beim Debugging über einen Debugger ist es wichtig, den Einstiegspunkt in die Applikation zu kennen. Wenn also zum Beispiel ein Dispatcher benutzt wird, um anhand des URL einen bestimmten Teil der Applikation aufzurufen und auszuführen, ist die Kenntnis des aufgerufenen URL sehr wichtig für einen schnellen Erfolg der Debuggings. Das Remote Debugging ermöglicht auch das „Surfen“ auf der Seite und in Verbindung mit einer IDE die direkte Interaktion des Debuggers mit der Seite.

Eine weitere Möglichkeit zur Eingrenzung des Fehlers ist die Verwendung der Linux/Unix-Programms Strace. Strace zeigt die Aufrufe an das System durch ein Programm an, solange es ausgeführt wird. Jede Zeile der Ausgabe von Strace zeigt einen Systemaufruf. Für die Eingrenzung des Fehlers auf eine Datei eines Projekts ist Strace interessant (Kasten: „Ausgabe von Strace“). Spannend ist auch im Fall eines „normalen“ PHP-Fehlers zu sehen was auf Systemebene passiert. Zum Beispiel wenn ein Cache nicht geschrieben werden kann weil die Dateirechte nicht richtig gesetzt sind oder eine Datei nicht vorhanden ist und versucht wird auf sie zuzugreifen. Das Debugging von Segmentation Faults mit Strace ist allerdings nur begrenzt möglich.

Ausgabe von Strace

Die Ausgabe von Strace erfolgt direkt nach stdout. Man kann mit der Option –o eine Datei angeben in der die Ausgabe gespeichert wird. Mit der Option –s 50000 wird sichergestellt, dass die Strings in der Ausgabe komplett sind. Die Pfade zu benutzten Dateien werden auch in der Normaleinstellung immer komplett gezeigt. Strace ist transparent für den Nutzer. Das heißt, dass bei einer Speicherzugriffsverletzung des mit Strace überwachten Programms auch Strace beendet wird:

strace php /tmp/reproduceSegFault.php

...
open("/tmp/reproduceSegFault.php", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0755, st_size=582, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbdb4d5a000
read(3, "<?phpnclass Test implements Seri"..., 4096) = 582
...
--- SIGSEGV (Segmentation fault) @ 0 (0) ---
+++ killed by SIGSEGV +++
Segmentation fault

Die Ausgabe zeigt den letzten open-Befehl vor dem Segmentation Fault und das Signal, das bei der Zugriffsverletzung vom Kernel erzeugt wurde.

Das Debugging eines Segmentation Faults ist mit einem C-Debugger komfortabler und aussagekräftiger als ein PHP-Debugger, einfache Debug-Ausgaben oder Strace. Im Linux/Unix-Bereich wird der Gnu-Debugger (gdb) eingesetzt, um Segmentation Faults zu finden. Im Vorgehen wird zwischen der Analyse eines Core Dumps, der automatisch nach dem Auftreten des Fehlers geschrieben wird, und dem direkten Debugging bei der Ausführung der fehlerhaften Applikation unterschieden.

Analyse eines Core Dumps mit gdb

Der Core Dump wird automatisch erzeugt, sobald ein Programm vom Kernel das SIGSEGV-Signal erhält. Allerdings müssen zur Sicherung und zur korrekten Analyse des Core Dumps einige Einstellungen vorgenommen werden. Zuerst muss PHP mit den Debug-Symbolen geladen werden. Die Debug-Symbole erlauben das Nachvollziehen einer Programmausführung. Ohne sie wäre es nicht möglich von einem Aufruf während der Laufzeit des Programms auf den Quellcode zu schließen. Für Ubuntu- und Debian-Linux-Betriebssysteme können die Debug-Symbole für PHP über das Paket php5-dbg mit sudo apt-get install php5-dbg bezogen werden. Leider scheint dieses Paket momentan im Debian Squeeze nicht zu funktionieren. Ein Aufruf von gdb/usr/bin/php5 erzeugt die Ausgabe in Listing 4 und zeigt, dass die Debug-Symbole nicht geladen wurden, da ein Mapping auf die PHP-Quelldateien nicht möglich ist. Daher ist es für den Test notwendig PHP neu zu kompilieren. Für das Ubuntu/Debian Linux ist das mit den Build-Tools möglich. Sobald die PHP5-Quelldateien im Verzeichnis liegen, muss nur noch die debian/rules-Datei angepasst werden. Hier wird einfach –disable-debug mit –enable-debug ausgetauscht, dann wird wie hier beschrieben der Build-Befehl ausgeführt. Die erzeugten Pakete können danach installiert und gegebenenfalls deinstalliert werden. Es ist wichtig, das selbst erzeugte Paket php5-dbg zu installieren. Mit der Option –enable-debug funktioniert die Auswertung dann einwandfrei.

Nicht korrekt geladene Debug-Symbole:
Listing 4

php5# gdb php
(gdb) run /tmp/reproduceSegFault.php
Starting program: /usr/bin/php /tmp/reproduceSegFault.php
[Thread debugging using libthread_db enabled]
object(Test)#1 (2) {
["member"]=>
*RECURSION*
["message"]=>
string(8) "original"
}

Program received signal SIGSEGV, Segmentation fault.
0x00000000007b64e1 in ?? ()

Korrekt geladene Debug-Symbole:

...
Program received signal SIGSEGV, Segmentation fault.
0x00000000007b64e1 in zend_parse_arg_impl (arg_num=Cannot access memory at address 0x7fffff7fefcc
) at /usr/src/php5-5.3.3/Zend/zend_API.c:289
289 {

Für andere Linux-Distributionen werden ähnliche Pakete zur Verfügung gestellt und können ebenfalls nachinstalliert werden. Wenn PHP selbst kompiliert ist, muss darauf geachtet werden, dass ein mit Debug-Symbolen (–enable-debug) konfiguriertes PHP ausgeführt wird, da erst dadurch eine Analyse über gdb sinnvoll möglich ist. Dem Kernel wird über die Linux-Kernel-Option core_patternmitgeteilt, dass ein Core Dump gesichert werden soll. Dabei wird das Verzeichnis in das der Core Dump geschrieben. Dieses Verzeichnis muss auch von dem Linux-Nutzer, unter dem der Webserver (z. B. www-data) läuft oder der das php-cgi ausführt, beschreibbar sein. Die Konfiguration muss als root-Nutzer erfolgen: echo „/tmp/core-%e.%p“ > /proc/sys/kernel/core_pattern. Ein Core Dump kann sehr groß werden und einige Linux-Distributionen limitieren die Größe eines Core Dumps. Daher ist es sinnvoll, vor einer Debug-Sitzung ein mögliches Systemlimit aufzuheben:ulimit -c unlimited.

Wenn alle Vorbereitungen getroffen sind, kann die Debug-Sitzung starten. Bei einer Analyse (Kasten: „Core-Dump-Analyse“) über den Webserver (Apache), wenn PHP als mod_php ausgeführt wird, muss er zunächst gestoppt werden, da die Ausführung durch die bereits laufende Webserverinstanz gestört würde. Anschließend wird der Webserver mit httpd –X gestartet und über einen Browser der Teil der Applikation aufgerufen, der den Fehler verursacht. Wenn noch nicht klar ist, welcher Teil der Applikation den Fehler verursacht, ist es oft hilfreich das Webserver-Access-Log zur Reproduktion des Fehlers zu nutzen. Tools wie JMeter erlauben den Import und das Abspielen eines Access Logs. Nachdem der Fehler reproduziert wurde, sollte sich im /tmp-Verzeichnis auf dem Rechner, auf dem der Webserver ausgeführt wurde, eine Datei mit dem Core Dump des Webservers befinden. Diese Datei kann mit dem GNU-Debugger aufgerufen werden. Für das Debuggen eines von einem PHP-CGI-Prozess gesicherten Core Dumps gilt genau das Gleiche. Es wird dann natürlich das PHP Binary zum Start des Debuggens benötigt. Mithilfe des Core Dumps und gdb können also über einige einfache Befehlsfolgen schnell die Funktion und die Datei ermittelt werden, die den Segmentation Fault verursacht hat.

Core-Dump-Analyse

Den Core Dump aufrufen:

gdb httpd –X /tmp/core.httpd.

Anzeige nach dem Aufruf:

Program terminated with signal 11, Segmentation fault.
#0 0x00000000007e22c5 in _zend_mm_alloc_canary_int (heap=0xfbcaa0, size=64,
__zend_filename=0xc58d40 "/usr/src/php5-5.3.3/Zend/zend_vm_execute.h", __zend_lineno=15130,
__zend_orig_filename=0xc51180 "/usr/src/php5-5.3.3/Zend/zend_alloc.c", __zend_orig_lineno=2723)
at /usr/src/php5-5.3.3/Zend/zend_alloc_canary.c:1947
1947 best_fit = zend_mm_search_large_block(heap, true_size);

Dieser Aufruf zeigt, mit welchem Fehlercode das Programm gestoppt hat und was der letzte ausgeführte Code war. Er ist mit dem Frame #0 gekennzeichnet. Zur Ermittlung der letzten verwendeten PHP-Funktion, die zum Programmabbruch geführt hat, ist es notwendig, so lange im Stack „nach unten“ zu gehen, bis eine Zeile mit dem String in execute gefunden wird. Dazu wird der Backtrace aufgerufen. Der Backtrace mit dem Befehl bt lässt sich folgendermaßen anzeigen:

(gdb) bt
...
#7 0x00000000007efffb in execute (op_array=0x12d1038)  at /usr/src/php5-5.3.3/Zend/zend_vm_execute.h:107
...

Der Backtrace zeigt, dass in Frame 7 (#7) das erste Mal ein in execute gezeigt wird. Daher wird dieser Frame näher analysiert. Der Frame mit dem ersten Vorkommen von in execute wird für die Analyse folgendermaßen geladen:

(gdb) frame 7
#7 0x00000000007efffb in execute (op_array=0x12d1038)  at /usr/src/php5-5.3.3/Zend/zend_vm_execute.h:107
107 if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
(gdb)

Zur Analyse, welche PHP-Funktion in welcher PHP-Datei den Fehler ausgelöst hat, werden an der Kommandozeile des Debuggers zwei Befehle zur Ermittlung des Funktionsnamens eingegeben:

(gdb) print (char *)executor_globals.active_op_array->function_name
$1 = 0x12d0c20 "serialize"

Es zeigt sich also, dass die PHP-Funktion serialize der Verursacher war. Interessant ist noch wo, also in welcher PHP-Datei, die Ausführung erfolgte. Dies wird mit dem folgenden Befehl ermittelt:

(gdb) print (char *)executor_globals.active_op_array->filename
$2 = 0x12cdc90 "/tmp/reproduceSegFault.php"
(gdb)

Der Fehler ist also in der PHP-Datei /tmp/reproduceSegFault.php aufgetreten.

Direkte Analyse während der Ausführung

In einigen Fällen ist es nicht erlaubt, im Betrieb einen Core Dump zu erzeugen oder den Server zu konfigurieren, damit ein Core Dump erzeugt werden kann. Damit dennoch ermittelt werden kann, welche PHP-Funktion einen Segmentation Fault verursacht hat, ist es möglich, den Fehler während der Ausführung des Programms näher zu betrachten. Das Vorgehen ist prinzipiell das gleiche wie bei der Analyse des Core Dumps. Nur der Einstieg ist anders. Es wird direkt mit dem Programm eine Debug-Session gestartet, mit der PHP ausgeführt wird, also entweder als mod_phpüber den Webserver, als CGI oder über die Kommandozeile (Kasten: „Direkte Analyse“).

Direkte Analyse

gdb httpd
(gdb) run –x

Jetzt kann mit dem Browser die Seite aufgerufen werden und im gdb wird wie bei der Analyse des Core Dumps ein Stack angezeigt. Hier wird ebenfalls das Vorkommen des ersten in executegesucht und dann genauso analysiert wie bei einem Core Dump. Mit CGI oder PHP an der Kommandozeile funktioniert das genauso:

gdb php
(gdb) run "/tmp/reproduceSegFault.php"

Das Ergebnis ist genau das gleiche wie bei einem Core Dump.

Wie geht es weiter, nachdem der Fehler lokalisiert wurde?

Sobald die fehlerhafte Funktion lokalisiert wurde, wird klar ob der Fehler aus einer Erweiterung oder dem PHP-Core heraus erzeugt wird. In beiden Fällen ist es hilfreich den PHP Bug Tracker zu durchsuchen ob in der PHP-Erweiterung mit der gerade genutzten Version ein Fehler aufgetreten ist. Die Versionen werden über php –i an der Kommandozeile oder durch einen Aufruf eines PHP Files mit der Funktion phpinfo() angezeigt und können dann einfach mit denen der Fehlerberichte des Bug Trackers verglichen werden. Das Ziel ist es herauszufinden, ob möglicherweise schon ein Patch für genau das Problem besteht. Falls kein Bericht zu dem Fehler besteht kann man einfach einen Bug-Bericht schreiben und sogar den Fehler selbst beheben. Damit das Projekt nicht beeinträchtigt wird, sollte geprüft werden ob ein Workaround (z. B. die Nutzung einer anderen PHP-Funktion) helfen kann, den Segmentation Fault zu vermeiden.

Fazit

Die Suche nach der Ursache eines Segmentation Faults ist mit dem Einsatz entsprechender Werkzeuge relativ einfach. Der Gnu-Debugger bietet für Unix/Linux- und Mac-Systeme eine schnelle Möglichkeit die Ursache für den Segmentation Fault zu finden. Dazu sind einige Vorbereitungen notwendig, die unter anderem PHP-Debug-Symbole voraussetzen. Die PHP Binaries mit Debug-Symbolen sollten dem Entwickler über den Entwicklungsprozess schnell und einfach zur Verfügung stehen. Dazu bieten die meisten Linux-Distributionen eine einfache Nachinstallation über das Paketmanagementsystem. Manchmal kann es schneller und einfacher sein mit einem PHP-Debugger das Problem einzugrenzen. Der PHP-Debugger oder die Debugging-Möglichkeit, mit echo, printoder vardump aus PHP heraus zu arbeiten, kann das Problem nur eingrenzen. Wenn das Problem lokalisiert wurde und eine Reproduktion möglich ist, ist der Bug Tracker von PHP eine gute Möglichkeit, auch der Community zu helfen, das Problem zu lösen. In der neuesten PHP-5-Version 5.3.8 für Debian Squeeze von dotdeb.org ist der Segmentation Fault übrigens nicht mehr zu reproduzieren.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -