PHP/Tutorials/Einführung in die Interna/Ein erster Blick auf die Zend-Engine
Inhaltsverzeichnis
Das Verarbeiten von PHP-Code
Im folgenden soll ein Überblick verschafft werden, wie die Zend-Engine funktioniert und aufgebaut ist. Am besten sieht man sich an, wie ein spezielles PHP-Script ausgeführt wird. Betrachten wir folgendes Beispiel:
<?php echo "Hallo" . "Welt!\n"; ?>
Als erstes wird der PHP-Code vom sogenannten Scanner oder auch Lexer verarbeitet. Der verarbeitet den Code in eine Reihe von sogenannten Tokens:
Code-Ausschnitt | Token | Beschreibung |
---|---|---|
<?php |
T_OPEN_TAG |
Der öffnende PHP-Tag (das Leerzeichen gehört mit dazu!) |
echo |
T_ECHO |
Ein echo -Statement (das ist in PHP keine Funktion, sondern eingebaut)
|
(Leerzeichen) | T_WHITESPACE |
Ein Freiraum (Leerzeichen, Zeilenumbrüche, etc.) |
"Hallo" |
T_CONSTANT_ENCAPSED_STRING |
Eine Zeichenkette, die im Text vorkommt |
(Leerzeichen) | T_WHITESPACE |
(s.o.) |
. |
'.' |
Für den Konkatenierungsoperator gibt es keinen spezifischen benannten Token, das Zeichen wird direkt zurückgegeben. |
(Leerzeichen) | T_WHITESPACE |
(s.o.) |
"Welt!\n" |
T_CONSTANT_ENCAPSED_STRING |
(s.o.) |
; |
';' |
Auch hier gibt es keinen benannten Token. |
(Leerzeichen) | T_WHITESPACE |
(s.o.) |
?> |
T_CLOSE_TAG |
Der schließende PHP-Tag |
Wichtig ist hier, dass alle benannten Tokens (das sind Konstanten im C-Code) Werte größer als 255 annehmen. Das heißt, dass einzelne Zeichen auch als Token zurückgegeben werden können (ein Byte kann 255 als maximalen Wert annehmen) und somit nicht für alles ein benannter Token benötigt wird – siehe hier zum Beispiel die Zeichen .
und ;
.
Als nächstes kompiliert PHP diese Sequenz aus Tokens in sogenannte Opcodes. Ein Opcode ist eine elementare Operation, die die Zend-Engine versteht, was man in etwa mit Bytecode aus Sprachen mit virtueller Maschine (Java, .NET-Sprachen) vergleichen kann oder zum Teil auch mit einer Assembler-Anweisung.
Wie auch bei Assembler-Anweisungen besteht ein Opcode aus mehreren Teilen:
- Der Anweisung, die ausgeführt werden soll.
- Optional: Einem Ergebnisoperanden (oder auch Ergebnisparameter), der angibt, wo das Ergebnis der Operation gespeichert werden soll.
- Optional: Bis zu zwei Eingabeoperanden, die angeben, woher Eingabeparameter für die Operation genommen werden sollen.
- Eventuell: Zusätzliche interne Flags für spezielle Opcodes.
Eine Sequenz von Opcodes bildet ein sogenanntes Op-Array. Ein Op-Array kann zum Beispiel eine Funktion sein oder auch einfach ein Stück Code, das direkt auf oberste Ebene in der PHP-Datei steht.
Nun kann man betrachten, in welche Opcodes der obige Beispiel-Code kompiliert wird. Da der hier dargestellte Opcode keine besonderen Flags benötigt, werden diese hier der Einfachheit halber weggelassen.
Nummer | Opcode | Ergebnis | Op 1 | Op 2 | Beschreibung |
---|---|---|---|---|---|
0 |
CONCAT |
temp. Variable #0 |
Konstante "Hallo" |
Konstante "Welt!\n" |
Diese Operation konkateniert die beiden Operanden (in diesem Fall zwei Konstanten, d. h. die Werte sind direkt schon beim Opcode abgelegt) und speichert das Ergebnis in einer temporären Variable. Temporäre Variablen kann man grob mit Registern in Assembler vergleichen. |
1 |
ECHO |
- | temp. Variable #0 |
- | Diese Operation gibt den Inhalt der temporären Variable 0 aus. Diese wird nach der Operation sofort zerstört.
|
2 |
RETURN |
- | Konstante 1 |
- | Dieser Opcode wird von PHP automatisch ans Ende von Code oder Funktionen angehängt, damit die Ausführungsschicht immer etwas zu tun hat, falls kein eigenes return im Code vorhanden ist.
|
Hinweis: In PHP-Versionen vor 5.3 gab es nach RETURN
immer noch einen Opcode ZEND_HANDLE_EXCEPTION
, der wegen des Exception-Handlings eingefügt wurde. Da dieses mit PHP 5.3 modifiziert wurde, ist dies nicht mehr nötig, also taucht er ab 5.3 nicht mehr auf, bei 5.2 dagegen schon.
Das nun erzeugte Op-Array wird dann der Ausführungsschicht (Executor) übergeben, die die Opcodes nacheinander abarbeitet und damit dann tatsächlich den PHP-Code effektiv ausführt.
Das folgende Diagramm veranschaulicht den Verarbeitungsvorgang von PHP-Code in der Zend-Engine noch einmal:
Zuerst wird der PHP-Code vom Scanner oder auch Lexer zu Tokens verarbeitet. Diese Tokens werden dann vom Compiler in Opcodes umgewandelt, die dann vom Executor ausgeführt werden.
Kompliziertere Opcodes
Natürlich wurde hier ein besonders einfaches Beispiel gewählt, um die wichtigsten Grundkonzepte darzustellen. Allerdings: Wenn man komplizierteren Code hat, dann gibt es auch kompliziertere Opcodes, um das abbilden zu können. In der Zend-Engine sind etwa 150 Opcodes definiert. Die Opcodes selbst werden allesamt in der Zend/zend_vm_def.h
definiert. In der Datei ist auch der Code zu finden, der bei jedem einzelnen Opcode ausgeführt wird.
Die Datei ist jedoch keine richtige C-Headerdatei, sondern nur eine Vorlage, die mit Hilfe des Scripts Zend/zend_vm_gen.php
in den eigentlichen Code umgewandelt werden kann. Dieses verarbeitet die Datei zusammen mit der Zend/zend_vm_execute.skl
zu den beiden Dateien Zend/zend_vm_execute.h
(enthält allen Code der bei Opcodes ausgeführt werden soll) und Zend/zend_vm_def.h
(enthält eine Auflistung aller Opcodes).
Kompliziertere Opcodes beinhalten zum Beispiel Sprünge. Diese sind bei if
-Abfragen nötig, bei Schleifen oder beim in PHP 5.3 neu eingeführten goto
. Es gibt verschiedene Sprung-Opcodes, die auf ihre jeweilige Aufgabe optimiert sind.
In PHP-Code definierte Funktionen und Klassen
Wenn ein ausgeführtes Stück PHP-Code Funktionen oder Klassen enthält, dann wird das ganze etwas komplizierter. Im folgenden werden der Einfachheit halber nur Funktionen betrachtet, für Klassen funktioniert dies jedoch analog.
Wenn ein Stück Code eine Funktion enthält, dann ist diese ja ein eigener Op-Array. Für jede Funktion, die im Quellcode definiert ist, legt der Compiler im globalen Funktionsnamensraum eine neue Funktion an, die jedoch einen speziellen Namen enthält, der eindeutig ist und aus Dateiname und Zeilennummer erzeugt wird. Über einen Trick wird dafür gesorgt, dass diese Funktion aus reinem PHP-Code nie aufrufbar ist. An der Stelle, wo die Funktion definiert wurde, hinterlässt der Compiler einen Opcode des Typs ZEND_DECLARE_FUNCTION
. Sobald der Executor an die Stelle kommt, wo der Opcode ist, wird die bereits existierende, aber versteckte Funktion unter ihrem richtigen Namen der Funktionstabelle hinzugefügt. Ab diesem Moment ist sie für PHP-Code aufrufbar. Auf diese Weise ist es möglich, dass man Funktionen innerhalb von if
-Abfragen definieren kann und PHP so wie erwartet reagiert.
Allerdings: Da die meisten Funktionen ja direkt im globalen Scope definiert werden, kennt der PHP-Compiler noch einen Trick: Wenn der Compiler feststellt, dass die Funktion im globalen Scope deklariert wurde, fügt der Compiler die Funktion schon vorab unter ihrem richtigen Namen hinzu und entfernt den Opcode von dieser Stelle.
Tools zur Arbeit mit den Interna von PHP
Um die Analyse der Interna von PHP zu erleichtern gibt es verschiedene Erweiterungen von PHP, die einen Blick ins Innere zulassen. Im folgenden werden drei Erweiterungen vorgestellt, die Licht auf den Kompiliervorgang werfen.
Die tokenizer
-Erweiterung
Um festzustellen, in welche Tokens der Scanner den PHP-Code wandelt gibt es die tokenizer
-Erweiterung, die direkt mit PHP mitgeliefert wird. Diese bietet zwei Funktionen an: Mit token_get_all
kann man von einem Stück Code die Tokens erhalten und mit token_name
kann man den zu einem Token zugehörigen Namen erhalten. Beispiel:
$tokens = '''token_get_all''' ('<?php echo "Hallo"."Welt!\n"; ?>');
echo "<table>";
foreach ($tokens as $token) {
if (is_array ($token)) {
$token_name = '''token_name''' ($token[0]);
$token_text = $token[1];
$token_line = $token[2];
} else {
$token_name = "'$token'";
$token_text = $token;
$token_line = null;
}
echo "<tr>";
echo "<td><code>".htmlspecialchars($token_text)."</code></td>";
echo "<td><code>".htmlspecialchars($token_name)."</code></td>";
echo "<td><code>".htmlspecialchars($token_line)."</code></td>";
echo "</tr>";
}
echo "</table>";
Der Vulcan Logic Disassembler
Der VLD oder auch Vulcan Logic Disassembler ist eine PECL-Erweiterung von Derick Rethans, die Informationen über Opcodes ausgibt während ein Script ausgeführt ist. Man muss lediglich die Erweiterung installieren, in der php.ini
laden und die Ini-Einstellung vld.active
aktivieren – dies kann man geschickt auf der Kommandozeile mit php -d vld.active=1 datei.php
tun, dann ist VLD per Default inaktiv aber bei Bedarf abrufbar. Die Ausgabe für das Testscript sieht so aus:
Branch analysis from position: 0 Add 0 Add 1 Add 2 Return found filename: /home/christian/... function name: (null) number of ops: 3 compiled vars: none line # op fetch ext return operands ------------------------------------------------------------------------------- 1 0 CONCAT RES[ IS_TMP_VAR ~0 ] OP1[ IS_CONST (7469231) 'Hallo' ] OP2[ ,IS_CONST (7469233) 'Welt%21%0A' ] 1 ECHO OP1[ IS_TMP_VAR ~0 ] 2 2 RETURN OP1[ IS_CONST (0) 1 ]
Der VLD gibt also direkt Informationen über die definierten Opcodes aus. Dies gibt also direkt das wieder, was der Executor später ausführt.
Die parsekit
-Erweiterung
Die parsekit-Erweiterung in PECL ist von der Funktionalität mit VLD vergleichbar, von der Bedienung dagegen mit der tokenizer
-Erweiterung: Man muss im PHP-Code selbst die Erweiterung aufrufen, um Informationen zu erhalten – dann jedoch erhält man diese Informationen schön als Array und kann sie in PHP direkt weiterverarbeiten.
Die Erweiterung bietet außerdem Zugriff auf viele interne Details der Op-Array- und Opcode-Datenstrukturen, die hier noch offenbart werden.
Im folgenden wird der Beispiel-Code von der parsekit
-Erweiterung verarbeitet:
<?php
var_dump (parsekit_compile_string ('echo "Hallo"."Welt!\n";'));
?>
Die Ausgabe dieses Codes ist folgende:
array(19) {
["type"]=>
int(4)
["type_name"]=>
string(14) "ZEND_EVAL_CODE"
["fn_flags"]=>
int(0)
["num_args"]=>
int(0)
["required_num_args"]=>
int(0)
["pass_rest_by_reference"]=>
bool(true)
["line_start"]=>
int(1515870810)
["line_end"]=>
int(1515870810)
["return_reference"]=>
bool(false)
["refcount"]=>
int(1)
["last"]=>
int(3)
["size"]=>
int(3)
["T"]=>
int(1)
["last_brk_cont"]=>
int(0)
["current_brk_cont"]=>
int(-1)
["backpatch_count"]=>
int(0)
["done_pass_two"]=>
bool(true)
["filename"]=>
string(17) "Parsekit Compiler"
["opcodes"]=>
array(3) {
[0]=>
array(8) {
["address"]=>
int(165759528)
["opcode"]=>
int(8)
["opcode_name"]=>
string(11) "ZEND_CONCAT"
["flags"]=>
int(197378)
["result"]=>
array(3) {
["type"]=>
int(2)
["type_name"]=>
string(10) "IS_TMP_VAR"
["var"]=>
int(0)
}
["op1"]=>
array(3) {
["type"]=>
int(1)
["type_name"]=>
string(8) "IS_CONST"
["constant"]=>
&string(5) "Hallo"
}
["op2"]=>
array(3) {
["type"]=>
int(1)
["type_name"]=>
string(8) "IS_CONST"
["constant"]=>
&string(6) "Welt!
"
}
["lineno"]=>
int(1)
}
[1]=>
array(6) {
["address"]=>
int(165759604)
["opcode"]=>
int(40)
["opcode_name"]=>
string(9) "ZEND_ECHO"
["flags"]=>
int(768)
["op1"]=>
array(3) {
["type"]=>
int(2)
["type_name"]=>
string(10) "IS_TMP_VAR"
["var"]=>
int(0)
}
["lineno"]=>
int(1)
}
[2]=>
array(7) {
["address"]=>
int(165759680)
["opcode"]=>
int(62)
["opcode_name"]=>
string(11) "ZEND_RETURN"
["flags"]=>
int(16777984)
["op1"]=>
array(3) {
["type"]=>
int(1)
["type_name"]=>
string(8) "IS_CONST"
["constant"]=>
&NULL
}
["extended_value"]=>
int(0)
["lineno"]=>
int(1)
}
}
}
GDB-Makros von PHP
PHP selbst liefert zudem noch einige Makros in einer .gdbinit
-Datei mit (diese wird automatisch vom GNU Debugger gdb eingelesen, wenn er im gleichen Verzeichnis ausgeführt wird), die einige nützliche Makros enthält, mit der man den laufenden Betrieb oder Coredumps von PHP analysieren kann. .gdbinit
findet sich im Hauptverzeichnis der PHP-Quellen.
Um dies sinnvoll nutzen zu können sind Kenntnisse in der Bedienung von gdb nötig. An geeigneter Stelle wird auf spezifische Makros eingegangen werden, da die Makros nur in Verbindung mit den jeweiligen Datenstrukturen zu verstehen sind.
Weblinks
- Nikita Popov: PHP 7 Virtual Machine