【PHP7源碼學習】2019-03-22 AST的遍歷

baiyanphp

所有視頻:https://segmentfault.com/a/11...node

原視頻地址:http://replay.xesv5.com/ll/24...segmentfault

引入

  • 先看上一節筆記中展現的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存儲中間結果
  • 具體圖解請參考以下兩篇參考文獻

參考資料

相關文章
相關標籤/搜索