执行过程

执行的入口函数为 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_varT 的数量分配 zval 大小的空间,以 11.3 节编译生成的指令集为例,分配的栈如图 11-20 所示。

image 2024 06 10 20 17 55 626
Figure 1. 图11-20 执行栈分配示意图

EG(vm_stack) 上分配的空间大小跟 op_arraylast_varT 的值相关。

初始化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;
}

从代码中可以看出,初始化工作主要做了几件事:

  1. 读取 op_array 中的第一条指令,赋值给 EX(opline),其中 EX 宏是对 execute_data 的取值宏;

  2. 设置 EX 的返回值;

  3. 赋值符号表;

  4. 设置运行时缓存;

  5. 设置常量数组。

做完这些工作后,执行栈中数据的结果如图11-21所示。

image 2024 06 10 20 25 13 961
Figure 2. 图11-21 初始化execute_data示意图

调用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 节中生成的指令集为例进行详细阐述。

  1. 对于第一条指令——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所示。

    image 2024 06 10 20 39 33 664
    Figure 3. 图11-22 Assign指令执行示意图

    完成 Assign 操作后,会调用 ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION 宏执行下一条指令,也就是 opline+1

  2. 第二条指令对应的是相加操作,其 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所示。

    image 2024 06 10 20 41 36 919
    Figure 4. 图11-23 Add指令执行示意图
  3. 第三条指令依然是 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所示。

    image 2024 06 10 20 43 06 106
    Figure 5. 图11-24 第二条Assign指令示意图
  4. 第四条指令是 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所示。

    image 2024 06 10 22 32 35 970
    Figure 6. 图11-25 Echo指令执行示意图
  5. 第五条指令为 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 虚拟机的执行过程。

释放execute_data

指令执行完毕后,调用 zend_vm_stack_free_call_frame 释放 execute_data,并回收 EG(vm_stack) 使用的空间,这部分比较简单。

到此,我们详细阐述了 AST 被编译成指令集,以及指令集被执行的过程,相信读者对 Zend 虚拟机有了一定了解。