Heim  >  Artikel  >  Backend-Entwicklung  >  PHP-Kernel-Analyse – ausführliche Erklärung der virtuellen Zend-Maschine

PHP-Kernel-Analyse – ausführliche Erklärung der virtuellen Zend-Maschine

黄舟
黄舟Original
2017-03-18 09:58:272213Durchsuche

PHP ist eine interpretierte Sprache. Für interpretierte Sprachen wie Java, Python, Ruby, Javascript wird der von uns geschriebene Code nicht in Maschinencode kompiliert und ausgeführt, sondern in Zwischencode kompiliert und ausgeführt auf der virtuellen Maschine (VM) auf. Die virtuelle Maschine, auf der PHP ausgeführt wird, wird als virtuelle Zend-Maschine bezeichnet. Heute werden wir tief in den Kernel eintauchen und die Funktionsprinzipien der virtuellen Zend-Maschine untersuchen.

OPCODE

Was ist OPCODE? Dabei handelt es sich um eine Anweisung, die die virtuelle Maschine erkennen und verarbeiten kann. Die virtuelle Maschine von Zend enthält eine Reihe von OPCODEs. Die virtuelle Maschine von OPCODE kann viele Dinge tun:

  • ZEND_ADD Zwei Operanden hinzufügen.

  • ZEND_NEW Erstellen Sie ein PHP-Objekt.

  • ZEND_ECHO Gibt den Inhalt auf der Standardausgabe aus.

  • ZEND_EXIT Beenden Sie PHP.

PHP hat 186 Operationen definiert (mit dem Update von PHP werden definitiv mehr Arten von OPCODE unterstützt. Die Definition und Implementierung aller OPCODEs finden Sie im Quellcode Datei (der Inhalt dieser Datei ist kein nativer C-Code, sondern eine Vorlage, der Grund wird später erklärt). zend/zend_vm_def.h

Sehen wir uns an, wie PHP die OPCODE-Datenstruktur entwirft:

struct _zend_op {
	const void *handler;
	znode_op op1;
	znode_op op2;
	znode_op result;
	uint32_t extended_value;
	uint32_t lineno;
	zend_uchar opcode;
	zend_uchar op1_type;
	zend_uchar op2_type;
	zend_uchar result_type;
};
Schauen Sie sich die Datenstruktur von OPCODE genau an und sehen Sie, ob Sie das Gefühl der Assemblersprache finden können. Jeder OPCODE enthält zwei Operanden,

und op1. Der op2-Zeiger zeigt auf die handler-Funktion , die die OPCODE-Operation ausführt, wird im -Medium gespeichert. result

Nehmen wir ein einfaches Beispiel:

<?php
$b = 1;
$a = $b + 2;
Wir sehen durch die vld-Erweiterung, dass der obige Code nach der Kompilierung den OPCODE der ZEND_ADD-Anweisung generiert.

compiled vars:  !0 = $b, !1 = $a
line     #* E I O op                           
fetch          
ext  
return  
operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 1
   3     1        ADD                                              ~3      !0, 2
         2        ASSIGN                                                   !1, ~3
   8     3      > RETURN                                                   1
Darunter ist die zweite Zeile der OPCODE des Befehls

. Wir sehen, dass es zwei Operanden empfängt, ZEND_ADD ist die Variable op1, $b ist die numerische Konstante 1 und das zurückgegebene Ergebnis wird in einer temporären Variablen gespeichert. In der Datei op2 finden wir die Funktionsimplementierung, die der Anweisung ZEND_ADD entspricht: zend/zend_vm_def.h

ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
{
	USE_OPLINE
	zend_free_op free_op1, free_op2;
	zval *op1, *op2, *result;

	op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
	op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
	if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
		if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
			result = EX_VAR(opline->result.var);
			fast_long_add_function(result, op1, op2);
			ZEND_VM_NEXT_OPCODE();
		} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
			result = EX_VAR(opline->result.var);
			ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
			ZEND_VM_NEXT_OPCODE();
		}
	} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {

	...
}
Der obige Code ist kein nativer C-Code, sondern eine Vorlage.

Warum machst du das? Denn PHP ist eine schwach typisierte Sprache und das darin implementierte C eine stark typisierte Sprache. Schwach typisierte Sprachen unterstützen den automatischen Typabgleich, und die Implementierung des automatischen Typabgleichs verarbeitet Parameter unterschiedlichen Typs genau wie der obige Code durch Beurteilung. Stellen Sie sich vor, wenn jeder OPCODE bei der Verarbeitung die Art der eingehenden Parameter bestimmen muss, wird die Leistung unweigerlich zu einem großen Problem (eine Anfrage muss möglicherweise Tausende von OPCODEs verarbeiten).

Gibt es eine Möglichkeit? Wir haben festgestellt, dass wir zur Kompilierungszeit bereits den Typ jedes Operanden bestimmen können (kann eine Konstante oder eine Variable sein). Wenn PHP den C-Code tatsächlich ausführt, werden daher verschiedene Arten von Operanden in verschiedene Funktionen unterteilt, die von der virtuellen Maschine direkt aufgerufen werden können. Dieser Teil des Codes wird in

platziert. Die erweiterte Datei ist ziemlich groß, und wir haben festgestellt, dass es auch solchen Code gibt: zend/zend_vm_execute.h

if (IS_CONST == IS_CV) {
Das macht überhaupt keinen Sinn, oder? Aber das spielt keine Rolle, der C-Compiler optimiert und beurteilt automatisch auf diese Weise. Wenn wir die Logik einer bestimmten OPCODE-Verarbeitung verstehen möchten, ist es in den meisten Fällen einfacher, die Vorlagendatei

zu lesen. Das Programm, das C-Code auf Basis von Templates generiert, ist übrigens in PHP implementiert. zend/zend_vm_def.h

Ausführungsprozess

Um genau zu sein, ist die Ausführung von PHP in zwei Teile unterteilt: Kompilierung und Ausführung. Ich werde hier nicht näher auf den Kompilierungsteil eingehen, sondern mich auf den Ausführungsprozess konzentrieren.

Nach einer Reihe von Kompilierungsvorgängen wie Grammatik- und Lexikonanalyse haben wir Daten namens OPArray erhalten, deren Struktur wie folgt ist:

struct _zend_op_array {
	/* Common elements */
	zend_uchar type;
	zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
	uint32_t fn_flags;
	zend_string *function_name;
	zend_class_entry *scope;
	zend_function *prototype;
	uint32_t num_args;
	uint32_t required_num_args;
	zend_arg_info *arg_info;
	/* END of common elements */

	uint32_t *refcount;

	uint32_t last;
	zend_op *opcodes;

	int last_var;
	uint32_t T;
	zend_string **vars;

	int last_live_range;
	int last_try_catch;
	zend_live_range *live_range;
	zend_try_catch_element *try_catch_array;

	/* static variables support */
	HashTable *static_variables;

	zend_string *filename;
	uint32_t line_start;
	uint32_t line_end;
	zend_string *doc_comment;
	uint32_t early_binding; /* the linked list of delayed declarations */

	int last_literal;
	zval *literals;

	int  cache_size;
	void **run_time_cache;

	void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
Viel Inhalt, oder? Einfaches Verständnis: Sein Kern besteht aus einem OPCODE-Array und einer Sammlung von Umgebungsdaten, die während der Ausführung erforderlich sind. Wir stellen einige relativ wichtige Felder vor:

  • Array, das OPCODE speichert. opcodes

  • Der Dateiname des aktuell ausgeführten Skripts. filename

  • Der Name der aktuell ausgeführten Methode. function_name

  • Statische Variablenliste. static_variables

  • last_try_catch Wenn im aktuellen Kontext eine Ausnahme auftritt, springt try-catch-finally zu den erforderlichen Informationen. try_catch_array

  • Eine Sammlung aller konstanten Literale wie der String foo oder die Zahl 23. literals

Warum müssen wir so große Datenmengen generieren? Denn je mehr Informationen beim Kompilieren generiert werden, desto weniger Zeit wird bei der Ausführung benötigt.

接下来,我们看下 PHP 是如何执行 OPCODE。OPCODE 的执行被放在一个大循环中,这个循环位于 zend/zend_vm_execute.h 中的 execute_ex 函数:

ZEND_API void execute_ex(zend_execute_data *ex)
{
	DCL_OPLINE

	zend_execute_data *execute_data = ex;

	LOAD_OPLINE();
	ZEND_VM_LOOP_INTERRUPT_CHECK();

	while (1) {
		if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) {
			if (EXPECTED(ret > 0)) {
				execute_data = EG(current_execute_data);
				ZEND_VM_LOOP_INTERRUPT_CHECK();
			} else {
				return;
			}
		}
	}

	zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn&#39;t happen");
}

这里,我去掉了一些环境变量判断分支,保留了运行的主流程。可以看到,在一个无限循环中,虚拟机会不断调用 OPCODE 指定的 handler 函数处理指令集,直到某次指令处理的结果 ret 小于0。注意到,在主流程中并没有移动 OPCODE 数组的当前指针,而是把这个过程放到指令执行的具体函数的结尾。所以,我们在大多数 OPCODE 的实现函数的末尾,都能看到调用这个宏:

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

在之前那个简单例子中,我们看到 vld 打印出的执行 OPCODE 数组中,最后有一项指令为 ZEND_RETURN 的 OPCODE。但我们编写的 PHP 代码中并没有这样的语句。在编译时期,虚拟机会自动将这个指令加到 OPCODE 数组的结尾。ZEND_RETURN 指令对应的函数会返回 -1,判断执行的结果小于0时,就会退出循环,从而结束程序的运行。

方法调用

如果我们调用一个自定义的函数,虚拟机会如何处理呢?

<?php
function foo() {
    echo &#39;test&#39;;
}

foo();

我们通过 vld 查看生成的 OPCODE。出现了两个 OPCODE 指令执行栈,是因为我们自定义了一个 PHP 函数。在第一个执行栈上,调用自定义函数会执行两个 OPCODE 指令:INIT_FC<a href="http://www.php.cn/wiki/1483.html" target="_blank">ALL</a> 和 DO_FCALL

compiled vars:  none
line     
#* E I O op                           
fetch          
ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP
   6     1        INIT_FCALL                                               &#39;foo&#39;
         2        DO_FCALL                                      
         0
         3      > RETURN                                                   1

compiled vars:  none
line     #* E I O op                           
fetch          
ext  
return  
operands
-------------------------------------------------------------------------------------
   3     0  E >   ECHO                                                     &#39;test&#39;
   4     1      > RETURN                                                   null

其中,INIT_FCALL 准备了执行函数时所需要的上下文数据。DO_FCALL 负责执行函数。DO_FCALL 的处理函数根据不同的调用情况处理了大量逻辑,我摘取了其中执行用户定义的函数的逻辑部分:

ZEND_VM_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL))
{
    USE_OPLINE
    zend_execute_data *call = EX(call);
    zend_function *fbc = call->func;
    zend_object *object;
    zval *ret;

    ...

    if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) {
        ret = NULL;
        if (RETURN_VALUE_USED(opline)) {
            ret = EX_VAR(opline->result.var);
            ZVAL_NULL(ret);
        }

        call->prev_execute_data = execute_data;
        i_init_func_execute_data(call, &fbc->op_array, ret);

        if (EXPECTED(zend_execute_ex == execute_ex)) {
            ZEND_VM_ENTER();
        } else {
            ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP);
            zend_execute_ex(call);
        }
    }

    ...

    ZEND_VM_SET_OPCODE(opline + 1);
    ZEND_VM_CONTINUE();
}

可以看到,DO_FCALL 首先将调用函数前的上下文数据保存到 call->prev_execute_data,然后调用 i_init_func_execute_data 函数,将自定义函数对象中的 op_array(每个自定义函数会在编译的时候生成对应的数据,其数据结构中包含了函数的 OPCODE 数组) 赋值给新的执行上下文对象。

然后,调用 zend_execute_ex 函数,开始执行自定义的函数。zend_execute_ex 实际上就是前面提到的 execute_ex 函数(默认是这样,但扩展可能重写 zend_execute_ex 指针,这个 API 让 PHP 扩展开发者可以通过覆写函数达到扩展功能的目的,不是本篇的主题,不准备深入探讨),只是上下文数据被替换成当前函数所在的上下文数据。

我们可以这样理解,最外层的代码就是一个默认存在的函数(类似 C 语言中的 main()函数),和用户自定义的函数本质上是没有区别的。

逻辑跳转

我们知道指令都是顺序执行的,而我们的程序,一般都包含不少的逻辑判断和循环,这部分又是如何通过 OPCODE 实现的呢?

<?php
$a = 10;
if ($a == 10) {
    echo &#39;success&#39;;
} else {
    echo &#39;failure&#39;;
}

我们还是通过 vld 查看 OPCODE(不得不说 vld 扩展是分析 PHP 的神器)。

compiled vars:  !0 = $a
line     #* E I O op                           
fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, 10
   3     1        IS_EQUAL                                         
   ~2      !0, 10
         2      > JMPZ                                                     ~2, ->5
   4     3    >   ECHO                                                     &#39;success&#39;
         4      > JMP                                                      ->6
   6     5    >   ECHO                                                     &#39;failure&#39;
   7     6    > > RETURN                                                   1

我们看到,JMPZ 和 JMP 控制了执行流程。JMP 的逻辑非常简单,将当前的 OPCODE 指针指向需要跳转的 OPCODE。

ZEND_VM_HANDLER(42, ZEND_JMP, JMP_ADDR, ANY)
{
	USE_OPLINE

	ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
	ZEND_VM_CONTINUE();
}

JMPZ 仅仅是多了一次判断,根据结果选择是否跳转,这里就不再重复列举了。而处理循环的方式与判断基本上是类似的。

<?php
$a = [1, 2, 3];
foreach ($a as $n) {
    echo $n;
}
compiled vars:  !0 = $a, !1 = $n
line     #* E I O op                           
fetch          
ext  return  
operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, <array>
   3     1      > FE_RESET_R                                       
   $3      !0, ->5
         2    > > FE_FETCH_R                                               $3, !1, ->5
   4     3    >   ECHO                                                     !1
         4      > JMP                                                      ->2
         5    >   FE_FREE                                                  $3
   5     6      > RETURN                                                   1

循环只需要 JMP 指令即可完成,通过 FE_FETCH_R 指令判断是否已经到达数组的结尾,如果到达则退出循环。

结语

通过了解 Zend 虚拟机,相信你对 PHP 是如何运行的,会有更深刻的理解。想到我们写的一行行代码,最后机器执行的时候会变成数不胜数的指令,每个指令又建立在复杂的处理逻辑之上。那些从前随意写下的代码,现在会不会在脑海里不自觉的转换成 OPCODE 再品味一番呢?

Das obige ist der detaillierte Inhalt vonPHP-Kernel-Analyse – ausführliche Erklärung der virtuellen Zend-Maschine. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn