Maison > développement back-end > tutoriel php > Analyse du noyau PHP-Explication détaillée de la machine virtuelle Zend

Analyse du noyau PHP-Explication détaillée de la machine virtuelle Zend

黄舟
Libérer: 2023-03-06 18:10:01
original
2278 Les gens l'ont consulté

PHP est un langage interprété. Pour les langages interprétés tels que Java, Python, Ruby et Javascript, le code que nous écrivons ne sera pas compilé en code machine à exécuter, mais sera compilé en code intermédiaire et exécuter sur la machine virtuelle (VM). La machine virtuelle qui exécute PHP s'appelle la machine virtuelle Zend. Aujourd'hui, nous allons approfondir le noyau et explorer les principes de fonctionnement de la machine virtuelle Zend.

OPCODE

Qu'est-ce qu'OPCODE ? C'est une instruction que la machine virtuelle peut reconnaître et traiter. La machine virtuelle Zend contient une série d'OPCODE. La machine virtuelle OPCODE peut faire beaucoup de choses. Voici quelques exemples d'OPCODE :

  • ZEND_ADD Ajouter deux opérandes.

  • ZEND_NEW Créez un objet PHP.

  • ZEND_ECHO Sort le contenu sur la sortie standard.

  • ZEND_EXIT Quittez PHP.

PHP a défini 186 opérations (avec la mise à jour de PHP, plus de types d'OPCODE seront certainement pris en charge. La définition et l'implémentation de tous les OPCODE peuvent être trouvées dans le code source fichier (le contenu de ce fichier n'est pas du code C natif, mais un modèle, la raison sera expliquée plus tard). zend/zend_vm_def.h

Jetons un coup d'œil à la façon dont PHP conçoit la structure de données OPCODE :

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;
};
Copier après la connexion
Regardez attentivement la structure de données d'OPCODE et voyez si vous pouvez trouver la sensation du langage assembleur. Chaque OPCODE contient deux opérandes,

et op1 Le pointeur op2 pointe vers la fonction handler qui effectue l'opération OPCODE. Le résultat du traitement de la fonction sera enregistré dans Medium. result

Prenons un exemple simple :

<?php
$b = 1;
$a = $b + 2;
Copier après la connexion
On voit à travers l'extension vld qu'après compilation, le code ci-dessus génère l'OPCODE de l'instruction ZEND_ADD.

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
Copier après la connexion
Parmi eux, la deuxième ligne est l'OPCODE de la commande

. On voit qu'il reçoit 2 opérandes, ZEND_ADD est la variable op1, $b est la constante numérique 1, et le résultat renvoyé est stocké dans une variable temporaire. Dans le fichier op2, on peut trouver l'implémentation de la fonction correspondant à l'instruction ZEND_ADD : 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)) {

	...
}
Copier après la connexion
Le code ci-dessus n'est pas du code C natif, mais un modèle.

Pourquoi fais-tu ça ? Parce que PHP est un langage faiblement typé et que le C, qui y est implémenté, est un langage fortement typé. Les langages faiblement typés prennent en charge la correspondance de type automatique et la mise en œuvre de la correspondance de type automatique, tout comme le code ci-dessus, gère les paramètres de différents types par jugement. Imaginez, si chaque OPCODE doit déterminer le type de paramètres entrants lors du traitement, alors les performances deviendront inévitablement un énorme problème (une requête peut devoir traiter des dizaines de milliers d'OPCODE).

Y a-t-il un moyen ? Nous avons constaté qu'au moment de la compilation, nous pouvons déjà déterminer le type de chaque opérande (il peut s'agir d'une constante ou d'une variable). Par conséquent, lorsque PHP exécute réellement le code C, différents types d'opérandes seront divisés en différentes fonctions pour un appel direct par la machine virtuelle. Cette partie du code est placée dans

. Le fichier développé est assez volumineux, et nous avons remarqué qu'il existe également un tel code : zend/zend_vm_execute.h

if (IS_CONST == IS_CV) {
Copier après la connexion
Cela n'a aucun sens, n'est-ce pas ? Mais ce n'est pas grave, le compilateur C optimisera et jugera automatiquement de cette façon. Dans la plupart des cas, si l'on veut comprendre la logique d'un certain traitement OPCODE, il est plus facile de lire le fichier modèle

. À propos, le programme qui génère du code C basé sur des modèles est implémenté en PHP. zend/zend_vm_def.h

Processus d'exécution

Pour être précis, l'exécution de PHP est divisée en deux parties : la compilation et l'exécution. Je ne développerai pas ici la partie compilation en détail, mais me concentrerai sur le processus d'exécution.

Après une série de processus de compilation tels que la grammaire et l'analyse lexicale, nous avons obtenu une donnée appelée OPArray, dont la structure est la suivante :

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];
};
Copier après la connexion
Beaucoup de contenu, n'est-ce pas ? Compréhension simple, son essence est un tableau OPCODE plus une collection de données environnementales requises lors de l'exécution. Présentation de plusieurs champs relativement importants :

  • Tableau stockant OPCODE. opcodes

  • Le nom de fichier du script actuellement exécuté. filename

  • Le nom de la méthode actuellement exécutée. function_name

  • Liste de variables statiques. static_variables

  • last_try_catch Dans le contexte actuel, si une exception se produit, try-catch-finally passe aux informations requises. try_catch_array

  • Une collection de tous les littéraux constants tels que la chaîne foo ou le nombre 23. literals

Pourquoi avons-nous besoin de générer des données aussi énormes ? Car plus il y a d’informations générées lors de la compilation, moins il faut de temps lors de l’exécution.

接下来,我们看下 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");
}
Copier après la connexion

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

ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
Copier après la connexion

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

方法调用

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

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

foo();
Copier après la connexion

我们通过 vld 查看生成的 OPCODE。出现了两个 OPCODE 指令执行栈,是因为我们自定义了一个 PHP 函数。在第一个执行栈上,调用自定义函数会执行两个 OPCODE 指令:INIT_FC<a href="//m.sbmmt.com/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
Copier après la connexion

其中,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();
}
Copier après la connexion

可以看到,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;;
}
Copier après la connexion

我们还是通过 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
Copier après la connexion

我们看到,JMPZJMP 控制了执行流程。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();
}
Copier après la connexion

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

<?php
$a = [1, 2, 3];
foreach ($a as $n) {
    echo $n;
}
Copier après la connexion
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
Copier après la connexion

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

结语

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal