AST编译过程

AST(抽象语法树)的编译是生成指令集 opcode 的过程,词法和语法分析后生成的 AST 会保存在 CG(ast) 中,然后 Zend 虚拟机会将 AST 进一步转换为 zend_op_array,以便在虚拟机中执行。下面我们讨论一下 AST 的编译过程。

编译过程在 zend_compile 函数中进行,该函数首先调用 zendparse 进行词法和语法分析,然后对 CG(ast) 的遍历,根据节点的不同类型编译为不同指令 opline,代码如下:

static zend_op_array *zend_compile(int type)
{
    /**代码省略**/
    if (! zendparse()) { //词法语法分析
        /**代码省略**/

        //初始化zend_op_array
        init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
        /**代码省略**/

        //遍历AST生成Opline
        zend_compile_top_stmt(CG(ast));
        /**代码省略**/

        //设置handler
        pass_two(op_array);
        /**代码省略**/
    }

    /**代码省略**/
    return op_array;
}

从上面的过程可以看出,编译的主要过程是 op_array 的初始化,调用 zend_compile_top_stmt 遍历 ASTopline,以及调用 pass_two 函数设置 handler。下面我们一一阐述。

op_array初始化

在遍历 AST 之前,需要先初始化指令集 op_array,用来存放指令。可通过调用函数 init_op_arrayop_array 进行初始化,代码如下:

op_array = emalloc(sizeof(zend_op_array));
init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)
{
    op_array->type = type;
    op_array->arg_flags[0] = 0;
    op_array->arg_flags[1] = 0;
    op_array->arg_flags[2] = 0;
    /**代码省略**/
}
CG(active_op_array) = op_array;

首先通过 emalloc 申请内存,大小为 sizeof(zend_op_array)=208 字节,然后初始化 op_array 的所有成员变量,把 op_array 赋值给 CG(active_op_array)

AST编译

AST 的编译过程是遍历 AST 生成对应指令集的过程,编译在 zend_compile_top_stmt 函数中完成,这个函数是总入口,会被多次递归调用。其中传入的参数为 CG(ast),这个 AST 是通过词法和语法分析得到的。下面我们看一下 zend_compile_top_stmt 的代码:

void zend_compile_top_stmt(zend_ast *ast) /* {{{ */
{
    if (! ast) {
        return;
    }
    //对于kind为ZEND_AST_STMT_LIST的节点,转换为zend_ast_list
    if (ast->kind == ZEND_AST_STMT_LIST) {
        zend_ast_list *list = zend_ast_get_list(ast);
        uint32_t i;
        //根据children的个数进行递归调用
        for (i = 0; i < list->children; ++i) {
            zend_compile_top_stmt(list->child[i]);
        }
        return;
    }
    //其他kind的节点调用zend_compile_stmt
    zend_compile_stmt(ast);

    if (ast->kind ! = ZEND_AST_NAMESPACE && ast->kind ! = ZEND_AST_HALT_COMPILER) {
        zend_verify_namespace();
    }
    if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
        CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
        zend_do_early_binding();
    }
}

从代码中可以看到,对于 zend_compile_top_stmt,会对 AST 节点的 kind 进行判断,然后走不同的逻辑,实际上是对 AST 的深度遍历。我们以下面的代码为例,看一下 AST 的遍历过程。

<?php
$a = 1;
$b = $a + 2;
echo $b;

根据第 10 章的知识,可以得到的 AST 如图11-11所示。

image 2024 06 10 19 33 57 288
Figure 1. 图11-11 AST 示意图

可以很直观地看出,CG(ast) 节点下面有 3 个子女。

  1. 第一个子女,其 kindZEND_AST_ASSIGN,有两个子女,分别是 ZEND_AST_VARZEND_AST_ZVAL,对应 $a=1

  2. 第二个子女,其 kind 也是 ZEND_AST_ASSIGN,有两个子女,分别是 ZEND_AST_VARZEND_AST_BINARY_OP,其中 ZEND_AST_BINARY_OP 对应的是相加操作,对应的是 $b=$a+2

  3. 第三个子女,其 kindZEND_AST_STMT_LIST,有一个子女,为 ZEND_AST_ECHO,对应的是 echo $b

下面我们来看整棵 AST 的遍历过程。

Assign编译过程

  1. 根节点 kindZEND_AST_STMT,会调用函数 zend_ast_get_list 将其转换为 zend_ast_list *,得到 children 的个数为 2,接着递归调用 zend_compile_top_stmt,这样就可以把 AST 根节点的最左子女遍历一遍,以便生成对应的指令。

  2. 遍历第一个子女节点,对应的 kindZEND_AST_ASSIGN,编译过程是调用函数 zend_compile_stmt,继而调用 zend_compile_expr 函数,代码如下:

    void zend_compile_stmt(zend_ast *ast) /* {{{ */
    {
        /*…代码省略…*/
            switch (ast->kind) {
                /*代码省略*/
                default:
                {
                    znode result;
                    zend_compile_expr(&result, ast);
                    zend_do_free(&result);
                }
                /*代码省略*/
        }
        void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */
        {
            /*代码省略*/
            switch (ast->kind) {
                /*代码省略*/
                case ZEND_AST_ASSIGN:
                    zend_compile_assign(result, ast);
                    return;
                /*代码省略*/
                }
        }

    最终调用函数 zend_compile_assign,对 ZEND_AST_ASSIGN 节点进行编译:

    void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */
    {
        zend_ast *var_ast = ast->child[0];
        zend_ast *expr_ast = ast->child[1];
    
        znode var_node, expr_node;
        zend_op *opline;
        uint32_t offset;
        /*代码省略*/
    
        switch (var_ast->kind) {
            case ZEND_AST_VAR:
            case ZEND_AST_STATIC_PROP:
                offset = zend_delayed_compile_begin();
                zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);
                zend_compile_expr(&expr_node, expr_ast);
                zend_delayed_compile_end(offset);
                zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);
                return;
            /*代码省略*/
            }
    }

    从代码中可以看出,kindZEND_AST_ASSIGNAST 有两个子女,左 childvar_ast,右 childexpr_ast,分别进行处理。

  3. 调用 zend_delayed_compile_begin

    static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */
    {
        return zend_stack_count (&CG(delayed_oplines_stack));
    }

    该函数会获取 CGdelayed_oplines_stack 栈顶的位置,其中 delayed_oplines_stack 是用来存储后续编译动作依赖信息的栈。用以在 expr_ast 编译后,通过调用 zend_delayed_compile_end(offset) 来获取栈里的信息。

  4. 对于左子女 var_ast,调用 zend_delayed_compile_var

    void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */
    {
        zend_op *opline;
        switch (ast->kind) {
            case ZEND_AST_VAR:
                zend_compile_simple_var(result, ast, type, 1);
        }
        /**代码省略**/
    }

    其中,kindZEND_AST_VAR,继而调用 zend_compile_simple_var 函数:

    static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type,
        int delayed) /* {{{ */
    {
        zend_op *opline;
    
        /*代码省略*/
        else if (zend_try_compile_cv(result, ast) == FAILURE) {
            /*代码省略*/
        }
    }

    继而调用 zend_try_compile_cv 函数:

    static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */
    {
        zend_ast *name_ast = ast->child[0];
        if (name_ast->kind == ZEND_AST_ZVAL) {
            /*代码省略*/
    
            result->op_type = IS_CV;
            result->u.op.var = lookup_cv(CG(active_op_array), name);
    
        }
        /*代码省略*/
    }

    核心函数是 lookup_cv,这里组装了操作数,代码如下:

    static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{
        int i = 0;
        zend_ulong hash_value = zend_string_hash_val(name);
        //判断变量是否在vars中存在,若存在直接返回对应的位置
        while (i < op_array->last_var) {
            if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) ||
                (ZSTR_H(op_array->vars[i]) == hash_value &&
                ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) &&
                memcmp(ZSTR_VAL(op_array->vars[i]),  ZSTR_VAL(name),  ZSTR_LEN(name))
                    == 0)) {
                    zend_string_release(name);
                    return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);
                }
            i++;
        }
        //若不存在,则写入vars中,返回新插入的位置
        i = op_array->last_var;
        op_array->last_var++;
        /*代码省略*/
    
        op_array->vars[i] = zend_new_interned_string(name);
    
        return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);
    }

    从代码中可以看出,变量是存放在 op_array->vars 中的,而返回的是一个 int 型的地址,这个是什么呢?我们看一下宏 ZEND_CALL_VAR_NUM 的定义:

    #define ZEND_CALL_VAR_NUM(call, n) \
        (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))
    #define ZEND_CALL_FRAME_SLOT \
        ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data))  +  ZEND_MM_ALIGNED_
            SIZE(sizeof(zval)) -1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))

    可以看出,这个值都是 sizeof(zval) 的整数倍,在笔者的机器上,zval 的大小为 16,而 zend_execute_data 的大小为 80,所以返回的是每个变量的偏移值,即 80+16i,如图11-12所示。

    image 2024 06 10 19 47 59 784
    Figure 2. 图11-12 左子女var_ast编译示意图

    此时,对于赋值语句 $a=1,左侧表达式 $a 编译完成,赋值给了 znode *result,下面继续对右子女常量 1 进行编译。

  5. 对于右子女,调用函数 zend_compile_expr 进行编译,代码如下:

    void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */
    {
        /* CG(zend_lineno) = ast->lineno; */
        CG(zend_lineno) = zend_ast_get_lineno(ast);
    
          switch (ast->kind) {
          case ZEND_AST_ZVAL:
              ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));
              result->op_type = IS_CONST;
              return;

    从代码中可以看出,对于常量 1,通过 ZVAL_COPY,将值复制到 result->u.constant 中,同时将 result->op_type 赋值为 IS_CONST。这样,对于 assign 操作,两个操作数都编译完成了,下面我们看一下对应指令 opline 的生成过程。

  6. opline 生成调用函数 zend_emit_op,代码如下:

    static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode
        *op2) /* {{{ */
    {
        //分配和获取opline,并设置其opcode
        zend_op *opline = get_next_op(CG(active_op_array));
        opline->opcode = opcode;
        //设置操作数1
        if (op1 == NULL) {
            SET_UNUSED(opline->op1);
        } else {
            SET_NODE(opline->op1, op1);
        }
        //设置操作数2
        if (op2 == NULL) {
            SET_UNUSED(opline->op2);
        } else {
            SET_NODE(opline->op2, op2);
        }
    
        zend_check_live_ranges(opline);
    
        if (result) {
            //设置返回值
            zend_make_var_result(result, opline);
        }
        return opline;
    }

    其中对操作数的设置,对应的是宏 SET_NODE,代码如下:

    #define SET_NODE(target, src) do {
            target ## _type = (src)->op_type;
            if ((src)->op_type == IS_CONST) {
                target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant);
            } else {
                target = (src)->u.op;
            }
        } while (0)
    
    int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */
    {
        int i = op_array->last_literal;
        op_array->last_literal++;
        if (i >= CG(context).literals_size) {
            while (i >= CG(context).literals_size) {
                CG(context).literals_size += 16; /* FIXME */
            }
            op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_
                size * sizeof(zval));
        }
        zend_insert_literal(op_array, zv, i);
        return i;
    }

    从代码中可以看出,对于操作数 1,会将编译过程中的临时结构 znode 传递给 zend_op;对于操作数 2,因为其是常量(IS_CONST),会调用 zend_add_literal 将其插入到 op_array->literals 中。

    对返回值的设置,调用的是 zend_make_var_result,其代码如下:

    static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */
    {
        //返回值的类型设置为IS_VAR
        opline->result_type = IS_VAR;
        //这个是返回值的编号,对应T位置
        opline->result.var = get_temporary_variable(CG(active_op_array));
        GET_NODE(result, opline->result);
    }
    static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */
    {
        return (uint32_t)op_array->T++;
    }

    返回值的类型为 IS_VAR, result.varT 的值,下面我们给出 Assign 操作对应的指令示意图,如图11-13所示。

    image 2024 06 10 20 03 49 885
    Figure 3. 图11-13 Assign指令示意图

    从图11-13可以看出,生成的 opline 中的 opcode 等于 38; op1 的类型为 IS_CV, op1.var 对应的是 vm_stack 上的偏移量;op2 的类型为 IS_CONST, op2.constant 对应的是 op_arrayliterals 数组的下标;result 的类型为 IS_VAR, result.var 对应的是 T 的值;此时 handler 的值为空。

Add编译过程

对于 “$b =$a+2; ” 语句,首先是 Add 语句,也就是 $a+1,跟 Assign 语句类型类似,不同的是调用了函数 zend_compile_binary_op,代码如下:

void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */
{
    zend_ast *left_ast = ast->child[0];
    zend_ast *right_ast = ast->child[1];
    uint32_t opcode = ast->attr; //通过attr区分加、减、乘、除等操作

    znode left_node, right_node;
    zend_compile_expr(&left_node, left_ast);
    zend_compile_expr(&right_node, right_ast);
    /*代码省略*/
    zend_emit_op_tmp(result, opcode, &left_node, &right_node);
    /*代码省略*/
}

对于加、减、乘、除等操作,kind 都是 ZEND_AST_BINARY_OP,具体操作通过 AST 中的 attr 区分,因为 $a+1 会生成临时变量,因此与 Assign 操作不同,调用的函数是 zend_emit_op_tmp

static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1,
    znode *op2) /* {{{ */
{
    /*代码与zend_emit_op一样*/
    if (result) {
        zend_make_tmp_result(result, opline);
    }

    return opline;
}

zend_emit_op_tmp 函数与 zend_emit_op 类似,opline 中的操作数 op1op2 做了同样的操作,而 result 的不同之处在于,其类型是 IS_TMP_VAR,因此 Add 指令示意图如图11-14所示。

image 2024 06 10 20 06 56 523
Figure 4. 图11-14 Add指令示意图

对于 “$b=$a+2; ”,相当于把临时变量赋值给 $b,与 Assign 编译过程一致,如图11-15所示。

image 2024 06 10 20 07 37 609
Figure 5. 图11-15 第二条 Assign 指令示意图

Echo编译过程

对于 “echo $b; ”,其编译过程类似于 AssignAdd,不同之处是调用的函数是 zend_compile_echo

void zend_compile_echo(zend_ast *ast) /* {{{ */
{
    zend_op *opline;
    zend_ast *expr_ast = ast->child[0];

    znode expr_node;
    zend_compile_expr(&expr_node, expr_ast);

    opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);
    opline->extended_value = 0;
}

Echo 对应的指令只有一个操作数,对于操作数 2, SET_UNUSED 宏设置为 IS_UNUSED

#define SET_UNUSED(op)  op ## _type = IS_UNUSED

Echo 指令示意图如图11-16所示。

image 2024 06 10 20 09 24 017
Figure 6. 图11-16 Echo指令示意图

Return编译过程

上面对 AST 的编译并没有结束,PHP 代码中虽然没有 return 操作,但是默认会生成一条 ZEND_RETURN 指令,通过 zend_emit_final_return 设置,代码如下:

void zend_emit_final_return(int return_one) /* {{{ */
{
    znode zn;
    zend_op *ret;
    /**代码省略**/

    zn.op_type = IS_CONST;
    if (return_one) {
        ZVAL_LONG(&zn.u.constant, 1);
    } else {
        ZVAL_NULL(&zn.u.constant);
    }

    ret =  zend_emit_op(NULL,  returns_reference  ?  ZEND_RETURN_BY_REF  :  ZEND_
          RETURN, &zn, NULL);
    ret->extended_value = -1;
}

同样通过 zend_emit_op 设置 oplineReturn 指令示意图如图11-17所示。

image 2024 06 10 20 10 52 635
Figure 7. 图11-17 Return指令示意图

经过对 AssignAddEcho 编译后,生成的全部 oplines 如图11-18所示。

image 2024 06 10 20 11 33 991
Figure 8. 图11-18 所有指令集示意图

到这里,我们了解了 AST 编译生成 opline 指令集的过程,包括 op1op2result 的生成过程,但是此时 opline 中的 handler 还是空指针,接下来我们看一下 handler 的设置过程。

设置指令handler

AST 编译后还有一个重要操作,即由函数 pass_twoopline 指令集做进一步的加工,其最主要的工作是设置指令的 handler,代码如下:

ZEND_API int pass_two(zend_op_array *op_array)
{
    /**代码省略**/
    while (opline < end) {//遍历opline数组
        if (opline->op1_type == IS_CONST) {
            ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);
        } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {
        opline->op1.var  =  (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL,  op_
            array->last_var + opline->op1.var);
        }

        if (opline->op2_type == IS_CONST) {
            ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2);
        } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) {
            opline->op2.var  =  (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL,
                op_array->last_var + opline->op2.var);
        }
    if (opline->result_type & (IS_VAR|IS_TMP_VAR)) {
        opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_
            array->last_var + opline->result.var);
    }
    ZEND_VM_SET_OPCODE_HANDLER(opline);
    /**代码省略**/
}

从代码中可以看出,该函数会对 opline 指令数组进行遍历,对每一条 opline 指令进行操作,对于 op1op2,如果其是 IS_CONST 类型,调用 ZEND_PASS_TWO_UPDATE_CONSTANT,代码如下:

/* convert constant from compile-time to run-time */
# define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do {
    (node).constant *= sizeof(zval);
} while (0)

IS_CONST 类型的变量的值存于 op_array->literals 数组中,因此,可以直接将数组下标乘以 sizeof(zval) 得到偏移量。

对于 op1op2,如果其是 IS_VAR 或者 IS_TMP_VAR 类型的变量,可以通过 ZEND_CALL_VAR_NUM 计算偏移量。

另外一个非常重要的工作是通过 ZEND_VM_SET_OPCODE_HANDLER(opline) 设置 opline 对应的 hanlder,代码如下:

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}

其中,opcodehandler 之前的对应关系在 Zend/zend_vm_execute.h 中定义。opline 数组经过一次遍历后,handler 即设置完毕,设置后的 opline 数组如图11-19所示。

image 2024 06 10 19 40 01 135
Figure 9. 图11-19 设置handler后的指令集

到此,整个 AST 就编译完成了,最终的结果为 opline 指令集,接下来在 Zend 虚拟机上执行这些指令。