1

baiyan

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

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

引入

  • 先看上一节笔记中展示的AST示例:
<?php
$a = 1;
$b = $a + 2;
  • 在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存储中间结果
  • 具体图解请参考如下两篇参考文献

参考资料


NoSay
449 声望544 粉丝