执行过程
执行的入口函数为 zend_execute
,该函数会针对生成的 opline
指令集进行调度执行。首先会在 EG(vm_stack)
上分配空间,然后每一条指令依次压栈并调用对应的 handler
。代码如下:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
/**代码省略**/
//压栈生成execute_data
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_
HAS_SYMBOL_TABLE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_
execute_data)), zend_get_this_object(EG(current_execute_data)));
//设置symbol_table
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data);
//初始化execute_data
i_init_execute_data(execute_data, op_array, return_value);
//执行
zend_execute_ex(execute_data);
//释放execute_data
zend_vm_stack_free_call_frame(execute_data);
}
在这个代码中,首先根据 op_array
中的指令生成对应的 execute_data
,然后初始化后调用 handler
执行。下面我们具体分析一下执行的过程。
执行栈分配
执行栈是通过11.2.6节介绍的 zend_vm_stack_push_call_frame
完成的,会在 EG(vm_stack)
上分配一块内存区域,80 字节用来存放 execute_data
,紧接着下面是根据 last_var
和 T
的数量分配 zval
大小的空间,以 11.3 节编译生成的指令集为例,分配的栈如图 11-20 所示。

在 EG(vm_stack)
上分配的空间大小跟 op_array
中 last_var
和 T
的值相关。
初始化execute_data
在执行栈上分配空间后,会调用函数 i_init_execute_data
对执行数据进行初始化,代码如下:
static zend_always_inline void i_init_execute_data(zend_execute_data *execute_
data, zend_op_array *op_array, zval *return_value) /* {{{ */
{
ZEND_ASSERT(EX(func) == (zend_function*)op_array);
EX(opline) = op_array->opcodes; //读取第一条指令
EX(call) = NULL;
EX(return_value) = return_value; //设置返回值
if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) {
//赋值符号表
zend_attach_symbol_table(execute_data);
/**代码省略**/
//运行时缓存
if (! op_array->run_time_cache) {
if (op_array->function_name) {
op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->
cache_size);
} else {
op_array->run_time_cache = emalloc(op_array->cache_size);
}
memset(op_array->run_time_cache, 0, op_array->cache_size);
}
EX_LOAD_RUN_TIME_CACHE(op_array);
EX_LOAD_LITERALS(op_array); //设置常量数组
EG(current_execute_data) = execute_data;
}
从代码中可以看出,初始化工作主要做了几件事:
-
读取
op_array
中的第一条指令,赋值给EX(opline)
,其中EX
宏是对execute_data
的取值宏; -
设置
EX
的返回值; -
赋值符号表;
-
设置运行时缓存;
-
设置常量数组。
做完这些工作后,执行栈中数据的结果如图11-21所示。

调用hanlder函数执行
接下来调用 execute_ex
执行指令,代码如下:
ZEND_API void execute_ex(zend_execute_data *ex)
{
ZEND_VM_LOOP_INTERRUPT_CHECK();
while (1) { //循环
int ret;
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;
}
}
}
从代码中可以看出,整个执行过程的最外层循环是 while
循环,直到结束才退出。该执行过程调用的是 opline
中对应的 handler
,下面以 11.3 节中生成的指令集为例进行详细阐述。
-
对于第一条指令——
Assign
指令,对应的handler
如下://ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER //通过op2获取到常量数组里面的值 value = EX_CONSTANT(opline->op2); //获取到op1对应的位置 variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); //将常量赋值给对应位置的指针 value = zend_assign_to_variable(variable_ptr, value, IS_CONST); //将结果复制到result ZVAL_COPY(EX_VAR(opline->result.var), value); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
首先通过
op2.constant
值获取常量表中的zval
值,通过op1.var
获取到栈中对应的位置,然后将常量值赋值到对应的位置,同时将其复制到result
对应的位置,如图11-22所示。Figure 3. 图11-22 Assign指令执行示意图完成
Assign
操作后,会调用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION
宏执行下一条指令,也就是opline+1
。 -
第二条指令对应的是相加操作,其
handler
如下://ZEND_ADD_SPEC_CV_CONST_HANDLER zval *op1, *op2, *result; //获取op1对应的位置 op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var); //获取op2对应的值 op2 = EX_CONSTANT(opline->op2); /**代码省略**/ //执行相加操作,赋值给result add_function(EX_VAR(opline->result.var), op1, op2); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
首先根据
op1.var
获取对应的位置,然后根据op2.constant
值获取常量表中的zval
值,最后进行相加操作,赋值给result
对应的位置,如图11-23所示。Figure 4. 图11-23 Add指令执行示意图 -
第三条指令依然是
Assign
,但是因为类型与第一条指令不同,因此对应的handler
也不同://ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER zval *value; zval *variable_ptr; //根据op2.var获取临时变量的位置 value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2); //根据op1.var获取操作数1 的位置 variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); //将临时变量赋值给操作数1对应的位置 value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR); //同时复制到result对应的位置 ZVAL_COPY(EX_VAR(opline->result.var), value); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
与第一条指令类似,执行过程如图11-24所示。
Figure 5. 图11-24 第二条Assign指令示意图 -
第四条指令是
Echo
操作,对应的handler
如下:// ZEND_ECHO_SPEC_CV_HANDLER zval *z; //根据op1.var获取对应位置的值 z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var); //调用zend_write输出 zend_write(ZSTR_VAL(str), ZSTR_LEN(str)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
这条指令会根据
op1.var
获取到对应的位置,取出zval
值输出,如图11-25所示。Figure 6. 图11-25 Echo指令执行示意图 -
第五条指令为
Return
,对应的handler
如下:
//ZEND_RETURN_SPEC_CONST_HANDLER
zval *retval_ptr;
zval *return_value;
retval_ptr = EX_CONSTANT(opline->op1);
return_value = EX(return_value);
//调用zend_leave_helper_SPEC函数,返回
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
这条指令没有做实质性的操作,核心是返回 -1,让 while
循环退出,指令执行结束。
到此,整个执行过程就介绍完了,相信读者通过这 5 条指令的执行,初步理解了 Zend
虚拟机的执行过程。