Home > Article > Backend Development > PHP kernel analysis-Zend virtual machine detailed explanation
PHP is an interpreted language. For interpreted languages such as Java, Python, Ruby, Javascript, the code we write will not be compiled into machine code for running, but will be compiled into intermediate code and run on the virtual machine ( VM) on. The virtual machine that runs PHP is called the Zend virtual machine. Today we will go deep into the kernel and explore the operating principles of the Zend virtual machine.
What is OPCODE? It is an instruction that the virtual machine can recognize and process. The Zend virtual machine contains a series of OPCODEs. The OPCODE virtual machine can do many things. Here are a few examples of OPCODEs:
##ZEND_ADD Combine two operands Add up.
ZEND_NEW Create a PHP object.
ZEND_ECHO Output the content to standard output.
ZEND_EXIT Exit PHP.
zend/zend_vm_def.h file (the content of this file is not native C code, but a template, the reason will be explained later).
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; };Look closely at the data structure of OPCODE and see if you can find the feeling of assembly language. Each OPCODE contains two operands,
op1 and
op2, and the
handler pointer points to the
function that performs the OPCODE operation. The result processed by the function will be saved in result.
<?php $b = 1; $a = $b + 2;We see through the vld extension that after compilation, the above code generates the OPCODE of the ZEND_ADD instruction.
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 1Among them, the second line is the OPCODE of the
ZEND_ADD instruction. We see that it receives 2 operands,
op1 is the variable
$b,
op2 is the numeric constant 1, and the returned result is stored in a temporary variable. In the
zend/zend_vm_def.h file, we can find the function implementation corresponding to the ZEND_ADD instruction:
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)) { ... }The above code is not native C code, but a template. Why did you do this? Because PHP is a weakly typed language, and the C language it implements is a strongly typed language. Weakly typed languages support automatic type matching, and the implementation of automatic type matching, just like the above code, handles parameters of different types through judgment. Just imagine, if each OPCODE needs to determine the type of incoming parameters when processing, then performance will inevitably become a huge problem (one request may need to process tens of thousands of OPCODEs). Is there any way? We found that at compile time, we can already determine the type of each operand (may be a constant or a variable). Therefore, when PHP actually executes the C code, different types of operands will be divided into different functions for direct call by the virtual machine. This part of the code is placed in
zend/zend_vm_execute.h. The expanded file is quite large, and we noticed that there is also such code:
if (IS_CONST == IS_CV) {It makes no sense at all, right? But it doesn't matter, the C compiler will automatically optimize and judge this way. In most cases, if we want to understand the logic of a certain OPCODE processing, it is easier to read the template file
zend/zend_vm_def.h. By the way, the program that generates C code based on templates is implemented in PHP.
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]; };There is a lot of content, right? Simple understanding, its essence is an OPCODE array plus a collection of environmental data required during execution. Introducing several relatively important fields:
opcodes Array to store OPCODE.
#filename The file name of the currently executed script.
function_name The name of the currently executed method.
static_variables Static variable list.
last_try_catch
try_catch_array In the current context, if an exception occurs try-catch-finally jumps to the required information.
literals A collection of all constant literals such as the string foo or the number 23.
接下来,我们看下 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'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 'test'; } 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 'foo' 2 DO_FCALL 0 3 > RETURN 1 compiled vars: none line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 3 0 E > ECHO 'test' 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 'success'; } else { echo 'failure'; }
我们还是通过 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 'success' 4 > JMP ->6 6 5 > ECHO 'failure' 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 再品味一番呢?
The above is the detailed content of PHP kernel analysis-Zend virtual machine detailed explanation. For more information, please follow other related articles on the PHP Chinese website!