资讯专栏INFORMATION COLUMN

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

RobinQu / 421人阅读

baiyan

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

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

引入

先看上一节笔记中展示的AST示例:


在PHP中,构造出来的抽象语法树如图所示:

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

指令的几个要素:

操作数:参与指令操作的变量或常量等,只需OP1和OP2最多两个操作数就够了,因为多元运算可以转化成二元运算(op1/op2)
指令操作:用来描述具体的赋值/加减乘除等指令操作(opcode)
返回值:用来存储中间运算结果(result)
处理函数:用来具体实现加减乘除等指令的操作逻辑(handler)

这些指令要素是我们自己定义的,而寄存器是无法理解这些自定义指令的,这就需要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; //下面oplines数组大小

      zend_op *opcodes; //oplines数组,存放所有指令

      int last_var;//操作数类型为IS_CV的个数

      uint32_t T;//操作数类型为IS_VAR和IS_TMP_VAR的个数之和

      zend_string **vars;//存放IS_CV类型操作数的数组

      ...

      int last_literal;//下面常量数组大小

      zval *literals;//存放IS_CONST类型操作数的数组

};

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/临时变量这些都是分配在栈上的(后面会讲)。通过计算,我们才能得出最终操作数在栈桢中的位置

在PHP7中,操作数有5种类型可选,如下:

#define IS_CONST        (1<<0)

#define IS_TMP_VAR      (1<<1)

#define IS_VAR          (1<<2)

#define IS_UNUSED       (1<<3)   /* Unused variable */

#define IS_CV           (1<<4)   /* Compiled variable */
IS_CONST类型:值为1,表示常量,如$a = 1中的1或者$a = "hello world"中的hello world
IS_TMP_VAR类型:值为2,表示临时变量,如$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果
IS_VAR类型:值为4,表示变量,但是这个变量并不是PHP中常见的声明变量,而是返回的临时变量,如$a = time()中的time()
IS_UNUSED:值为8,表示没有使用的操作数
IS_CV:值为16,表示形如$a这样的变量

既然如何我们知道了如何存储指令,那么下面讲一下如何遍历这棵抽象语法树,来得到这些指令集:

遍历抽象语法树 编译根节点(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;            /* 存储变量的值 */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(            //大小端问题,详情看"PHP内存管理3笔记”
                zend_uchar    type,        //注意这里就是存放变量类型的地方,char类型
                zend_uchar    type_flags,  //类型标记
                zend_uchar    const_flags, //是否是常量
                zend_uchar    reserved)       //保留字段
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* 数组模拟链表,链地址法解决哈希冲突时使用 */
        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)这种语法,不加括号可以吗?
为什么要do{}while(0),反正都是只执行一次这个宏的代码,可以去掉do{}while(0)吗?

针对第一个问题,是出于安全性的考虑,看下面一个例子:

#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中的值:

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

具体图解请参考如下两篇参考文献

参考资料

【PHP7源码分析】PHP7源码研究之浅谈Zend虚拟机

【PHP7源码分析】如何理解PHP虚拟机(一)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31491.html

相关文章

  • 【每日学习记录】使用录像设备记录每天学习

    摘要:在这里使用学而思网校的录像设备,记录每天学习的内容闫昌李乐阶段李乐李乐李乐李乐李乐李乐马运运李乐李乐李乐源码集群闫昌源码闫昌源码主从复制李乐源码施洪宝源码施洪宝韩天 在这里使用学而思网校的录像设备,记录每天学习的内容: 2019-06-24 ~ 2019-06-28 06-27 nginx by 闫昌 06-26 nginx module by 李乐 06-25 nginx http ...

    szysky 评论0 收藏0
  • 【每日学习记录】使用录像设备记录每天学习

    摘要:在这里使用学而思网校的录像设备,记录每天学习的内容执行潘森执行潘森执行潘森赵俊峰红黑树景罗红黑树景罗配置三叉树田志泽新建模块马运运配置田志泽田志泽田志泽李乐田志泽田志泽文件系统 在这里使用学而思网校的录像设备,记录每天学习的内容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 执行 by 潘森 07-17 nginx http 执行 by 潘森 07...

    pkhope 评论0 收藏0

发表评论

0条评论

RobinQu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<