【PHP源码学习】2019-03-22 AST的遍历-1

栏目: PHP · 发布时间: 5年前

内容简介:baiyan全部视频:原视频地址:

【PHP源码学习】2019-03-22 AST的遍历-1

baiyan

全部视频: https://segmentfault.com/a/11...

原视频地址: http://replay.xesv5.com/ll/24...

引入

  • 先看上一节笔记中展示的AST示例:
<?php
$a = 1;
$b = $a + 2;
  • PHP 中,构造出来的抽象语法树如图所示:

【PHP源码学习】2019-03-22 AST的遍历-1

  • 那么,这个AST后面能够用来做什么呢?因为我们最终需要执行这段PHP代码,所以需要将其转化为可执行的指令,让虚拟机最终来解释执行
  • 指令的几个要素:

操作数(OP1、OP2,两个就够了,因为多元运算可以转化成二元运算)

指令操作(handler)

运算的中间结果 返回值 (result)

  • 这些指令要素是我们自己定义的,而寄存器是无法理解这些自定义指令的,这就需要 zend虚拟机(zendvm) 去进行指令转换工作并且真正的执行这些指令。

举例:$a = 1 + 2; 这行代码中,$a/1/2是操作数,1+2计算的中间结果3是返回值,加法和赋值是指令做的具体操作,加法对应加法的opcode与相应的handler,赋值对应赋值的opcode与相应的handler

  • 接下来讲一下这些指令在PHP7中是如何存储的:

指令的基本概念及存储结构

  • 在PHP的zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成。每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),而每个opcode又对应一个 handler 处理函数。这样,zend虚拟机就可以根据生成的指令,找到对应的指令处理函数,把操作数作为参数传入,即可完成指令的执行

基本概念总结

opline:在zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成

opcode:每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),在PHP7中,有100多种指令操作,所有的指令集被称作opcodes

handler:每个opcode指令操作都对应一个 handler 指令处理函数,处理函数中有具体的指令操作执行逻辑

  • 下面看一下PHP内部的具体实现,回想昨天调试的zend_compile断点处,它是编译的入口,并返回生成结果op_array:
static zend_op_array *zend_compile(int type)
{
    zend_op_array *op_array = NULL;
    zend_bool original_in_compilation = CG(in_compilation);

    CG(in_compilation) = 1;//CG宏可以取得compile_globals结构体中的字段
    CG(ast) = NULL;//一开始的AST为NULL
    CG(ast_arena) = zend_arena_create(1024 * 32);//给AST分配空间

    if (!zendparse()) { //进行词法和语法分析
    
        .......
        //初始化op_array,用来存放指令
        op_array = emalloc(sizeof(zend_op_array));
        init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE);
        CG(active_op_array) = op_array;

        ......
        //对AST进行遍历并生成指令,并存储到zend_op_array中
        zend_compile_top_stmt(CG(ast)); 
        
        ......
         //设置handler
        pass_two(op_array);
    
    }
    return op_array;
}
  • 重点关注zend_op_array这个类型,它是一个数组,用来存储所有的指令集(即所有的opline)。来看看它的结构:
struct _zend_op_array {
    ...
    uint32_t last; //数组的长度
    zend_op *opcodes; //存储所有opline的值
    ...
};
  • zend_op_array是指令的集合,那么每条指令在zendvm中是一个opline。它由 指令操作、操作数、返回值、以及指令操作对应的handler 组成。每一条指令opline对应的结构体是zend_op:
struct _zend_op {
    const void *handler; //操作
    znode_op op1; //操作数1
    znode_op op2; //操作数2
    znode_op result; //操作结果
    uint32_t extended_value;
    uint32_t lineno; //行号
    zend_uchar opcode; //opcode值
    zend_uchar op1_type; //操作数1类型
    zend_uchar op2_type; //操作数2类型
    zend_uchar result_type; //返回值类型
};
  • 每一条指令opline,对应一个zend_op,存放指令集的zend_op_array由多条zend_op所构成
  • 现在我们知道了指令中的具体操作是由opcode和handler来表示和处理,那么继续看一下 操作数返回值 具体是如何表示的,如zend_op结构体中所示,它们的类型均为znode_op类型:
typedef union _znode_op {
    uint32_t      constant;
    uint32_t      var;
    uint32_t      num;
    uint32_t      opline_num; /*  Needs to be signed */
#if ZEND_USE_ABS_JMP_ADDR
    zend_op       *jmp_addr;
#else
    uint32_t      jmp_offset;
#endif
#if ZEND_USE_ABS_CONST_ADDR
    zval          *zv;
#endif
} znode_op;
  • 可以看到,constant、var、num都是uint32类型的,这个uint32类型并不足以表示所有操作数。这里存储的是 相对于执行栈帧首地址的偏移量 。因为CV/临时变量这些都是分配在栈上的(后面会讲)。通过计算,我们才能得出最终操作数在栈桢中的位置
  • 既然如何我们知道了如何存储指令,那么下面讲一下如何遍历这棵抽象语法树,来得到这些指令集:

遍历抽象语法树

编译根节点(LIST)

我们现在仅仅关注$a = 1这一行代码并对其AST进行遍历

  • 关注zend_compile中的zend_compile_top_stmt(CG(ast))这个函数调用,它会对这棵AST进行遍历:
void zend_compile_top_stmt(zend_ast *ast)
{
    if (!ast) {
        return;
    }

    if (ast->kind == ZEND_AST_STMT_LIST) { //如果是这个AST是LIST类型
        zend_ast_list *list = zend_ast_get_list(ast);//将其转换成ZEND_AST_LIST类型即可(上一篇笔记是在gdb下直接强转的)
        uint32_t i;
        for (i = 0; i < list->children; ++i) {
            zend_compile_top_stmt(list->child[i]); //递归调用,进行深度遍历
        }
        return;
    }

    zend_compile_stmt(ast); //编译的入口。递归调用的时候,如果往下走的时候并非LIST型结点,会调用这个函数

    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();
    }
}
  • 内联函数(inline):普通的函数调用是需要压栈的,而内联函数直接将函数体代码嵌入到调用位置,提高代码执行效率
  • 具体代码执行过程(按照最开始的AST图来讲):

    • 根节点进来,判断是ZEND_AST_LIST类型(132),故将其强转成ZEND_AST_LIST类型
    • 递归调用函数本身,参数传入其第一个子结点(517,赋值运算符)
    • 赋值运算符不是LIST类型,往下走到zend_compile_stmt(ast)中
  • 接下来看下编译入口zend_compile_stmt()的具体实现:
void zend_compile_stmt(zend_ast *ast) 
{
    if (!ast) {
        return;
    }

    CG(zend_lineno) = ast->lineno;

    if ((CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) && !zend_is_unticked_stmt(ast)) {
        zend_do_extended_info();
    }

    switch (ast->kind) {
        case ZEND_AST_STMT_LIST:
            zend_compile_stmt_list(ast);
            break;
        ......
        default: //最终会走到这里,因为所有的case中,没有ZEND_AST_ASSIGN类型与之匹配
        {
            znode result; //声明了一个znode类型变量,存储返回值(像$a = 1 + 2)这种需要存储中间结果3的表达式才需要使用这个result
            zend_compile_expr(&result, ast); //调用这个函数处理当前表达式,下面会展开
            zend_do_free(&result);
        }
    }
    ......
}
  • 代码最终会走到default分支。会首先声明一个znode类型的变量result,看一下znode类型的结构:
typedef struct _znode {  
    zend_uchar op_type;
    zend_uchar flag;
    union {
        znode_op op; //操作数变量的位置
        zval constant; //常量
    } u;
} znode;
  • 重点关注这个联合体u中的op以及constant字段,在后面可以用来存储编译过程中的中间值,先记下这个结构体
  • 接下来会调用zend_compile_expr函数,继续跟进zend_compile_expr(&result, ast),注意这里的ast是517位根节点的子树而非最开始的ast。看一下这个函数的具体实现:
void zend_compile_expr(znode *result, zend_ast *ast) 
{
    ......
    switch (ast->kind) {
        ......
        case ZEND_AST_ASSIGN:
            zend_compile_assign(result, ast); //代码走到这里,调用这个函数,下面继续跟进
            return;
        ......
    }
}

编译赋值(ASSIGN)等号

  • 最终上面代码会走到ZEND_AST_ASSIGN的case中,并调用zend_compile_assign(result, ast)函数,继续跟进这个函数:
void zend_compile_assign(znode *result, zend_ast *ast)
{
    zend_ast *var_ast = ast->child[0]; //517的第一个孩子256
    zend_ast *expr_ast = ast->child[1]; //517的第二个孩子64

    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); //编译$a,中间结果放到var_node这个znode上
            zend_compile_expr(&expr_node, expr_ast); //编译1,中间结果放到expr_node这个znode上
            zend_delayed_compile_end(offset);
            zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成opline
            return;
        ......
    }
}
  • 首先取出赋值等号(517)的第一个子结点,其类型是ZEND_AST_VAR(256),然后取出第二个子结点,其类型是ZEND_AST_ZVAL(64),switch之后来到ZEND_AST_STATIC_PROP这个case,直接看第二行这个函数zend_delayed_compile_var,它的功能是编译左边的$a

编译赋值等号左边的$a

  • 接下来调用zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); 这个函数被用来编译左边的$a,它会将编译产生的中间结果放到var_node这个znode中:
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);
            return;
        ...
    }
}
  • 代码会执行到ZEND_AST_VAR这个case中,然后调用zend_compile_simple_var(result, ast, type, 1),继续跟进:
static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) 
{
    zend_op *opline;

    if (is_this_fetch(ast)) {
        ......
    } else if (zend_try_compile_cv(result, ast) == FAILURE) {
        ......
    }
}
  • 首先第一个if中is_this_fetch(ast)是判断是否和this(对象)相关,我们这里不是,那么走到下一个else if分支,调用zend_try_compile_cv(result, ast)函数,继续跟进:
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) {
        zend_string *name = zval_get_string(zend_ast_get_zval(name_ast));

        if (zend_is_auto_global(name)) {
            zend_string_release(name);
            return FAILURE;
        }

        result->op_type = IS_CV; //将其类型标记为CV,CV变量在运行时是存在栈上的
        result->u.op.var = lookup_cv(CG(active_op_array), name); //返回这个CV变量在运行时栈上的偏移量

        name = CG(active_op_array)->vars[EX_VAR_TO_NUM(result->u.op.var)];

        return SUCCESS;
    }

    return FAILURE;
}
  • 这个函数首先取它的第一个孩子结点,因为当前传过来的ast是256(ZEND_AST_VAR类型),它的孩子结点只有一个,且类型为64(ZEND_AST_ZVAL类型),所以第一个if判断为true,并且调用了zval_get_string(zend_ast_get_zval(name_ast))这个函数。我们由内往外看,首先对这个64的ast结点进行zend_ast_get_zval()函数调用,它会将ZEND_AST类型转化成ZEND_AST_ZVAL类型,和之前在gdb中调试的效果一样。接下来外部对这个ast结点调用zval_get_string()函数,看下它的内部实现:
static zend_always_inline zend_string *_zval_get_string(zval *op) {
    return Z_TYPE_P(op) == IS_STRING ? zend_string_copy(Z_STR_P(op)) : _zval_get_string_func(op);
}
  • 首先,Z_TYPE_P这个宏取得zval中的u1.v.type字段,如果是IS_STRING的话,调用zend_string_copy(z_str_p(op))函数,首先调用了Z_STR_P这个宏,它会取得zend_value中的str字段,就是指向zend_string的指针,我们可以看到以下上面宏的定义:
/* we should never set just Z_TYPE, we should set Z_TYPE_INFO */
#define Z_TYPE(zval)                zval_get_type(&(zval))
#define Z_TYPE_P(zval_p)            Z_TYPE(*(zval_p))

static zend_always_inline zend_uchar zval_get_type(const zval* pz) {
    return pz->u1.v.type;
}

...
#define Z_STR(zval)                    (zval).value.str
#define Z_STR_P(zval_p)                Z_STR(*(zval_p))
  • 我们继续看一下外层zend_string_copy()的实现:
static zend_always_inline zend_string *zend_string_copy(zend_string *s)
{
    if (!ZSTR_IS_INTERNED(s)) {
        GC_REFCOUNT(s)++;
    }
    return s;
}
  • 我们看到仅仅是做了一个对zend_string中的refcount字段++的操作,并没有真正去做具体的拷贝
  • 回到zend_try_compile_cv()这个函数,我们在调用完之后将结果赋值给name变量。接下来因为变量不是全局的,所以不进这个if。接下来对result变量的两个字段进行了赋值操作:
result->op_type = IS_CV;
result->u.op.var = lookup_cv(CG(active_op_array), name);
  • 首先为它赋值IS_CV。那么什么是CV型变量呢?CV,即compiled variable,是PHP编译过程中产生的一种变量类型,以类似于缓存的方式,提高某些变量的存储速度。这里$a = 1中的$a,就是CV型变量,CV型变量在运行时是存储在zend_execute_data(后面会讲)虚拟机上的栈中的
  • 第二行,给result中的u.op.var字段赋值,而result我们讲过,是一个znode。重点关注右边lookup_cv()函数,它返回一个int类型的地址,是sizeof(zval)的整数倍,通过它可以得到每个变量的偏移量(80(后面会讲) + 16 * i),i是变量的编号。这样就规定了运行时在栈上相对于zend_execute_data的偏移量,从而在栈上方便地存储了$a这个变量(下一篇笔记会详细讲)。而$a在zend_op_array的vars数组上也冗余存了一份,这样如果后面又用到了$a的话,直接去zend_op_array的vars数组中查找找,如果存在,那么直接使用之前的编号i,如果不存在则按序分配一个编号,然后再插入zend_op_array的vars数组,节省了分配编号的时间
static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{
    int i = 0;
    zend_ulong hash_value = zend_string_hash_val(name);

    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*));
    }

    op_array->vars[i] = zend_new_interned_string(name);
    return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i);
}

编译赋值等号右边的值1

  • 接下来回到外部zend_compile_assign函数,继续往下执行zend_compile_expr(&expr_node, expr_ast)函数,处理等号右边的值1,它会将编译产生的中间结果放到expr_node这个znode中:
  • 接下来会走到zend_compile_expr函数的ZEND_AST_ZVAL这个case,看下这个case:
case ZEND_AST_ZVAL:
            ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast));
            result->op_type = IS_CONST;
            return;
  • 它调用了一个ZVAL_COPY宏,将这个ZEND_AST_ZVAL类型的结点的zval字段中存储的值(即1对应的zval),拷贝到之前声明的znode类型变量result的u.constant字段中,这样操作数就存放完毕了
  • 看一下这个zend_ast_get_zval(ast)的具体实现:
static zend_always_inline zval *zend_ast_get_zval(zend_ast *ast) {
    ZEND_ASSERT(ast->kind == ZEND_AST_ZVAL);
    return &((zend_ast_zval *) ast)->val;
}
  • 先忽略断言,它直接利用强转并取出val字段的值,就是1对应的zval,并返回了它的地址
  • 接下来再看一下ZVAL_COPY这个宏,它的功能是把一个zval(v)拷贝到另外一个zval(z)中
  • 我们首先回顾一下zval的结构:
struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,            /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)        /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
        uint32_t     extra;                /* not further specified */
    } u2;
};
  • 由zval结构,我们可以知道:复制zval,就是将老zval中的 value/u1/u2 三个字段拷贝到新zval中即可。
  • 那么,源码中是怎么实现的呢:
#define ZVAL_COPY(z, v)                                    \
    do {                                                \
        zval *_z1 = (z);                                \
        const zval *_z2 = (v);                            \
        zend_refcounted *_gc = Z_COUNTED_P(_z2);        \
        uint32_t _t = Z_TYPE_INFO_P(_z2);                \
        ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t);            \
        if ((_t & (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)) != 0) { \
            GC_REFCOUNT(_gc)++;                            \
        }                                                \
    } while (0)
  • 首先将z赋值给_z1,它是一个地址,然后将v赋值给_z2,也是一个地址,注意这个地址的值是常量,表示不能够修改v指向的zval的值(因为它是被赋值的zval,所以没必要修改它的值)
  • 然后通过Z_COUNTED_P(_z2)这个宏取出_z2(v)这个zval中的refcounted字段,看下这个宏的具体实现:
#define Z_COUNTED(zval)                (zval).value.counted
#define Z_COUNTED_P(zval_p)            Z_COUNTED(*(zval_p))
  • 可以看到,它取出了zval中的zend_value字段中的counted字段的值,那么,为什么要取counted这个字段呢?我们回顾一下zend_value的结构:
typedef union _zend_value {
    zend_long         lval;    //整型
    double            dval;    //浮点
    zend_refcounted  *counted; //引用计数,这里取出这个字段的值
    zend_string      *str; //字符串
    zend_array       *arr; //数组
    zend_object      *obj; //对象
    zend_resource    *res; //资源
    zend_reference   *ref; //引用
    zend_ast_ref     *ast; //抽象语法树
    zval             *zv;  //内部使用
    void             *ptr; //不确定类型,取出来之后强转
    zend_class_entry *ce;  //类
    zend_function    *func;//函数
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww; //这个union一共8B,这个结构体每个字段都是4B,因为所有联合体字段共用一块内存,故相当于取了一半的union
} zend_value;
  • 这里一定要注意zend_value是一个 联合体 。由于其内部所有字段共用一块内存空间,源码中取counted字段的值,和取lval/dval/str/arr这些字段值的效果是 完全一样 的,其本质上就是取到了zval中的zend_value这个字段的值。
  • 那么现在我们完成了从老的zval中取到zend_value这个字段的值,还剩下u1/u2两个字段需要我们去拿
  • 接下来它使用了Z_TYPE_INFO_P(_z2);这个宏,看下它的实现:
#define Z_TYPE_INFO(zval)            (zval).u1.type_info
#define Z_TYPE_INFO_P(zval_p)        Z_TYPE_INFO(*(zval_p))
  • 它直接取到了zval中u1的type_info字段。由于u1也是一个联合体,实际上就是取得了v这个结构体中的type/type_flags/const_flags/reserved这四个字段的值,理由同上。这样,u1也拿到了,那么现在还剩下u2没有去取
  • 接下来又去调用了ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t)这个宏,我们看下它的实现:
# define ZVAL_COPY_VALUE_EX(z, v, gc, t)                \
    do {                                                \
        Z_COUNTED_P(z) = gc;                            \
        Z_TYPE_INFO_P(z) = t;                            \
    } while (0)
#else
  • 它就是直接将gc.counted字段,即zend_value的值)、以及t(即u1的值)直接拷贝到z这个zval中,这样就完成了zend_value以及u1的复制,那么u2为什么没有拷贝呢?因为u2这个联合体中的字段并不重要,不对其进行复制不会对代码逻辑有任何影响
  • 下面的if我们可以先忽略,由于在ZVAL_COPY_VALUE_EX宏中完成了复制,可以不去考虑下面的逻辑
  • 那么现在针对这个宏中出现的语法,提两个问题:

为什么会出现(z)这种语法,不加括号可以吗?

  • 针对第一个问题,是出于安全性的考虑,看下面一个例子:
#define X(a, b) \ 
    a = b * 3;
  • 如果我们这样调用宏:X(a, 1+2);那么宏展开的结果为 a = 1 + 2 3 ,即a = 7 ,而我们预期的结果是a = (1+2) 3 = 9,出现了运算符优先级的问题,所以当传入的参数是一个表达式的时候,不加括号会出现 运算符优先级 不符合预期的问题,所以加上括号能够更加安全
  • 下面看第二个为什么要加上do-while(0)的问题,扩展一下上面的宏X:
#define X(a, b) \ 
    a = b * 3;
    a  = a + 1;
  • 那么我们如果在代码中编写:
if (true)
    X(a, b)
  • 那么它的效果等同于:
if (true)
    a = b * 3;
    a = a +1;
  • 可以看到,如果if不加{}大括号的话, *只会执行第一条语句a = b 3*,并不符合预期
  • 如果加上do{}while(0),其效果等同于:
if (true) 
    do {
        a = b * 3;
        a = a +1;
    } while(0)
  • 由于do-while语句的存在,两个表达式变成了 一个整体 ,所以宏体中两个表达式都会被执行。所以,这样做能够保证 宏体里所有语句都会被完整的执行
  • 目前为止,变量$a和表达式1的信息,均已存储在两个不同的znode中,均已编译完成,下面就开始生成指令了:

指令的生成

  • 接下来回到外部zend_compile_assign函数,继续往下执行zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);这个函数,它负责整合之前编译过程中的中间结果,并生成相应的指令:
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */
{
    zend_op *opline = get_next_op(CG(active_op_array));
    opline->opcode = opcode;

    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);
    }

    zend_check_live_ranges(opline);

    if (result) {
        zend_make_var_result(result, opline);
    }
    return opline;
}
  • 首先观察传递进去的参数,注意这里的result并非之前给result赋值的那个result(那个result是var_node和expr_node这两个临时znode),这个result在之前外层的default分支声明还未赋值,为NULL。第二个就是指令所要执行的操作opcode,即ASSIGN;第三个参数和第四个参数是操作数,我们这里就是$a和1,也将其存储编译期间信息的znode传递进去
  • 有了操作数$a、指令操作(ASSIGN)、操作数(1),我们现在就可以生成指令了
  • 一条指令是一个opline,且opline是存储在zend_op_array上的。那么我们首先在zend_op_array上分配一个opline出来用于存储即将生成的指令。随后将opcode的信息也赋值到这个opline上去。那么现在opline上只有一个opcode,还没有操作数$a和操作数(1)的信息,也没有result的信息。因为op1不是空,调用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)
  • 从SET_NODE(opline->op1, op1)的两个参数可以看出,它将操作数$a的信息拷贝到opline上
  • op2也不为空,也与op1同理,将值1的znode信息拷贝到opline上
  • 下面zend_check_live_ranges这个函数先忽略,那么现在还剩下一个返回值没有设置,由于我们这里result的值还是空,所以不进这个if。因为我们$a = 1这个简单的赋值表达式,是没有返回值这一说法的。但是类似$a = 1 + 2;这样的表达式,返回的中间值3的信息可以存在result这个znode中,然后同样拷贝到opline上,这个时候才会用到result。这个函数的细节不再展开
  • 最后全部编译完之后,看下这几个znode中的值:

【PHP源码学习】2019-03-22 AST的遍历-1

  • 由于CV型变量是存储在zend_execute_data栈桢上的,故图中的80即是$a在执行栈桢上的偏移量,通过计算能够找到$a。1是等号右侧表达式1的值,而result中的值没有意义,在这里没有使用result存储中间结果
  • 具体图解请参考如下两篇参考文献

参考资料


以上所述就是小编给大家介绍的《【PHP源码学习】2019-03-22 AST的遍历-1》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

XML 基础教程

XML 基础教程

(美)雅可布斯 / 许劲松 等 / 人民邮电出版社 / 2007-7 / 49.00元

《XML 基础教程:入门、DOM、Ajax与Flash》全面讲述了XML及其在Web开发领域中的作用,同时介绍了一些特定的XML词汇以及相关的XML推荐标准。书中首先解释了XML并介绍了XML文档的不同组成部分;其次讲解了XML应用程序客户端的处理方法,如何使用CSS和 XSLT对XML文档进行显示和转换,如何使用JavaScript操作XML文档等内容;然后介绍了如何在服务器端处理XML;最后深......一起来看看 《XML 基础教程》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具