AST->zend_op_array

3.1.2.2 AST->zend_op_array

上面我们介绍了zend_op_array结构,接下来我们回过头去看下语法解析(zendparse())之后的流程:

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
    zend_op_array *op_array = NULL; //编译出的opcodes
    ...

    if (open_file_for_scanning(file_handle)==FAILURE) {//文件打开失败
        ...
    } else {
        zend_bool original_in_compilation = CG(in_compilation);
        CG(in_compilation) = 1;

        CG(ast) = NULL;
        CG(ast_arena) = zend_arena_create(1024 * 32);
        if (!zendparse()) { //语法解析
            zval retval_zv;
            zend_file_context original_file_context; //保存原来的zend_file_context
            zend_oparray_context original_oparray_context; //保存原来的zend_oparray_context,编译期间用于记录当前zend_op_array的opcodes、vars等数组的总大小
            zend_op_array *original_active_op_array = CG(active_op_array);
            op_array = emalloc(sizeof(zend_op_array)); //分配zend_op_array结构
            init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE);//初始化op_array
            CG(active_op_array) = op_array; //将当前正在编译op_array指向当前
            ZVAL_LONG(&retval_zv, 1);

            if (zend_ast_process) {
                zend_ast_process(CG(ast));
            }

            zend_file_context_begin(&original_file_context); //初始化CG(file_context)
            zend_oparray_context_begin(&original_oparray_context); //初始化CG(context)
            zend_compile_top_stmt(CG(ast)); //AST->zend_op_array编译流程
            zend_emit_final_return(&retval_zv); //设置最后的返回值
            op_array->line_start = 1;
            op_array->line_end = CG(zend_lineno);
            pass_two(op_array);
            zend_oparray_context_end(&original_oparray_context);
            zend_file_context_end(&original_file_context);

            CG(active_op_array) = original_active_op_array;
        }
        ...
    }
    ...

    return op_array;
}

compile_file()操作中有几个保存原来值的操作,这是因为这个函数在PHP脚本执行中并不会只执行一次,主脚本执行时会第一次调用,而include、require也会调用,所以需要先保存当前值,然后执行完再还原回去。

AST->zend_op_array编译是在 __zend_compile_top_stmt()__ 中完成,这个函数是总入口,会被多次递归调用:

//zend_compile.c
void zend_compile_top_stmt(zend_ast *ast)
{
    if (!ast) {
        return;
    }

    if (ast->kind == ZEND_AST_STMT_LIST) { //第一次进来一定是这种类型
        zend_ast_list *list = zend_ast_get_list(ast);
        uint32_t i;
        for (i = 0; i < list->children; ++i) {
            zend_compile_top_stmt(list->child[i]);//list各child语句相互独立,递归编译
        }
        return;
    }

    //各语句编译入口
    zend_compile_stmt(ast);

    if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {
        zend_verify_namespace();
    }
    //function、class两种情况的处理,非常关键的一步操作,后面分析函数、类实现的章节再详细分析
    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(); //很重要!!!
    }
}

首先从AST的根节点开始编译,根节点类型为ZEND_AST_STMT_LIST,这个类型表示当前节点下有多个独立的节点,各child都是独立的语句生成的节点,所以依次编译即可,直到到达有效节点位置(非ZEND_AST_STMT_LIST节点),然后调用zend_compile_stmt编译当前节点:

void zend_compile_stmt(zend_ast *ast)
{
    CG(zend_lineno) = ast->lineno;

    switch (ast->kind) {
        case xxx:
            ...
        break;
        case ZEND_AST_ECHO:
            zend_compile_echo(ast);
            break;
        ...
        default:
        {
            znode result;
            zend_compile_expr(&result, ast);
            zend_do_free(&result);
        }
    }

    if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) {
        zend_emit_tick();
    }
}

主要根据不同的节点类型(kind)作不同的处理,我们不会把每种类型的处理都讲一遍,这里还是根据上一节最后的例子挑几个看下具体的处理过程。

$a = 123;
$b = "hi~";
echo $a,$b;

zendparse()阶段生成的AST:

zend_ast

下面的过程比较复杂,有的函数会多次递归调用,我们根据例子一步步去看下,如果你对PHP各个语法实现比较熟悉再去看整个AST的编译过程就会比较轻松。

> (1)、 首先从根节点开始,有3个child,第一个节点类型为ZEND_AST_ASSIGN,zend_compile_stmt()中走到default分支

> (2)、 ZEND_AST_ASSIGN类型由zend_compile_expr()处理:

void zend_compile_expr(znode *result, zend_ast *ast)
{
    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;
        case ZEND_AST_VAR:
            zend_compile_var(result, ast, BP_VAR_R);
            return;
        case ZEND_AST_ASSIGN:
            zend_compile_assign(result, ast);
            return;
        ...
    }
}

> 继续进入zend_compile_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;

    if (is_this_fetch(var_ast)) { //检查变量名是否为this,变量名不能是this
        zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this");
    }

    //比如这样写:my_function() = 123;即:将函数的返回值作为变量名将报错
    zend_ensure_writable_variable(var_ast);

    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); //生成变量名的znode,这个结构只在这个地方临时用,所以直接分配在stack上
            zend_compile_expr(&expr_node, expr_ast); //递归编译变量值表达式,最终需要得到一个ZEND_AST_ZVAL的节点
            zend_delayed_compile_end(offset);
            zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成一条op
            return;
        ...
    }
}

> 这个地方主要有三步关键操作:

>> 第1步: 变量赋值操作有两部分:变量名、变量值,所以首先是针对变量名的操作,介绍zend_op_array时曾提到每个PHP变量都有一个编号,变量的读写都是根据这个编号操作的,这个编号最早就是这一步生成的。

> 中间过程我们不再细看,这里重点看下变量编号的过程,这个过程比较简单,每发现一个变量就遍历zend_op_array.vars数组,看此变量是否已经保存,没有保存的话则存入vars,然后后续变量的使用都是用的这个变量在数组中的下标,比如第一次定义的时候:$a = 123;将$a编号为0,然后:echo $a;再次使用时会遍历vars,发现已经存在,直接用其下标操作$a。

static int lookup_cv(zend_op_array *op_array, zend_string* name)
{
    int i = 0;
    zend_ulong hash_value = zend_string_hash_val(name);

    //遍历op_array.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++;
    }
    //这是一个新变量
    i = op_array->last_var;
    op_array->last_var++;
    if (op_array->last_var > CG(context).vars_size) {
        CG(context).vars_size += 16; /* FIXME */
        op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*));//扩容vars
    }

    op_array->vars[i] = zend_new_interned_string(name);
    return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); //传NULL时返回的是96 + i*sizeof(zval)
}

> 注意:这里变量的编号从0、1、2、3...依次递增的,但是实际使用中并不是直接用的这个下标,而是转化成了内存偏移量offset,这个是ZEND_CALL_VAR_NUM宏处理的,所以变量偏移量实际是96、112、128...递增的,这个96是根据zend_execute_data大小设定的(不同的平台下对应的值可能不同),下一篇介绍zend执行流程时会详细介绍这个结构。__

#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))))

#define ZEND_CALL_VAR_NUM(call, n) \
    (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n))))

>> 第2步: 编译变量值表达式,再次调用zend_compile_expr()编译,示例中的情况比较简单,expr_ast.kind为ZEND_AST_ZVAL:

void zend_compile_expr(znode *result, zend_ast *ast)
{
    switch (ast->kind) {
        case ZEND_AST_ZVAL:
            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); //将变量值复制到znode.u.constant中
            result->op_type = IS_CONST; //类型为IS_CONST,这种value后面将会保存在zend_op_array.literals中
            return;
        ...
    }
}

> 第3步: 上面两步已经分别生成了变量赋值的op1、op2,下面就是根据这俩值生成opcode的过程。

tatic zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2)
{
    zend_op *opline = get_next_op(CG(active_op_array)); //当前zend_op_array下生成一条新的指令
    opline->opcode = opcode;

    //将op1、op2内容拷贝到zend_op中,设置op_type
    //如果znode.op_type == IS_CONST,则会将znode.u.contstant值转移到zend_op_array.literals中
    if (op1 == NULL) {
        SET_UNUSED(opline->op1);
    } else {
        SET_NODE(opline->op1, op1);
    }

    if (op2 == NULL) {
        SET_UNUSED(opline->op2);
    } else {
        SET_NODE(opline->op2, op2);
    }

    //如果此指令有返回值则想变量那样为返回值编号(后面分配局部变量时将根据这个编号索引)
    if (result) {
        zend_make_var_result(result, opline);
    }
    return opline;
}

static inline void zend_make_var_result(znode *result, zend_op *opline)
{
    opline->result_type = IS_VAR; //返回值类型固定为IS_VAR
    opline->result.var = get_temporary_variable(CG(active_op_array)); //为返回值编个号,这个编号记在临时变量T上,上面介绍zend_op_array时说过T、last_var的区别
    GET_NODE(result, opline->result);
}

>> 到这我们示例中的第1条赋值语句就算编译完了,第2条同样是赋值,过程与上面相同,我们直接看最好一条输出的语句。

> (3)、 echo语句的编译:echo $a,$b;实际从编译后的语法树就可以看出,一次echo多个也被编译为多次echo了,所以示例中的用法与:echo $a; echo $b;等价,我们只分析其中一个就可以了。

> zend_compile_stmt()中首先发现节点类型是ZEND_AST_STMT_LIST,然后调用zend_compile_stmt_list()分别编译child,具体的流程如下图所示:

> 最后生成zend_op的过程:

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);//生成1条新的opcode
    opline->extended_value = 0;
}

最终zend_compile_top_stmt()编译完成后整个编译流程基本是完成了,CG(active_op_array)结构如下图所示,但是后面还有一个处理pass_two()

ZEND_API int pass_two(zend_op_array *op_array)
{
    zend_op *opline, *end;

    if (!ZEND_USER_CODE(op_array->type)) {
        return 0;
    }

    //重置一些CG(context)的值,暂且忽略
    ...

    opline = op_array->opcodes;
    end = opline + op_array->last;
    while (opline < end) {
        switch(opline->opcode){
            //这里对一些操作进行针对性的处理,后面有遇到的情况我们再看
            ...
        }

        //如果是IS_CONST会将数组下标转化为内存偏移量,与IS_CV那种处理方式相同
        //所以这里实际就是将0、1、2...转为为16、32、48...(即:编号*sizeof(zval))
        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)) {
            //上面作相同的处理,不同的是这里的起始值是接着IS_CV的
            opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var);
        }
        //与op1完全相同
        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);
        }
        //返回值与op1/2相同处理
        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);
        }
        //设置此opcode的处理handler
        ZEND_VM_SET_OPCODE_HANDLER(opline);
        opline++;
    }

    //标识当前op_array已执行过此操作
    op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
    return 0;
}

抛开特殊opcode的处理,pass_two()主要有两个重要操作:

  • (1)将IS_CONST、IS_VAR、IS_TMP_VAR类型的操作数、返回值转化为内存偏移量,与上面提到的IS_CV变量的处理一样,其中IS_CONST类型起始值为0,然后按照编号依次递增sizeof(zval),而IS_VAR、IS_TMP_VAR唯一的不同时它的初始值接着IS_CV的,简单的讲就是先安排PHP变量的,然后接着才是各条语句的中间值、返回值
  • (2)另外一个重要操作就是设置各指令的处理handler,这个前面《3.1.2.1.1 handler》已经介绍过其索引规则

经过pass_two()处理后opcodes的样子:

总结:

到这里整个PHP编译阶段就算全部完成了,最终编译的结果就是zend_op_array,其中最核心的操作就是AST的编译了,有兴趣的可以多写几个例子去看下不同节点类型的处理方式。

另外,编译阶段很关键的一个操作就是确定了各个 变量、中间值、临时值、返回值、字面量内存编号 ,这个地方非常重要,后面介绍执行流程时也会用到。

联系我们

邮箱 626512443@qq.com
电话 18611320371(微信)
QQ群 235681453

Copyright © 2015-2024

备案号:京ICP备15003423号-3