【PHP7源碼學習】抽象語法樹編譯過程的驗證

grapephp

引入

閱讀前請先查看【PHP源碼學習】2019-03-22 AST的遍歷瞭解基本概念。node

此篇文章是針對於$a=1進行了gdb實戰調試,以驗證 【PHP源碼學習】2019-03-22 AST的遍歷 中的一些論證。segmentfault

實驗代碼:數組

<?php
    $a =1;

基本流程:

-   zend_compile_top_stmt(CG(ast))
-   zend_compile_stmt(ast)
-   zend_compile_expr(&result, ast);
-   zend_compile_assign(result, ast);
-   zend_delayed_compile_begin();
-   zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W);
-   zend_compile_expr(&expr_node, expr_ast);
-   zend_delayed_compile_end(offset);
-   zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node);

GDB過程

咱們來gdb一下整個過程,首先,在zend_compile_top_stmt入口處打斷點:函數

clipboard.png

gdb下來咱們進入:源碼分析

clipboard.png

在zend_compile_stmt咱們進入default的zend_compile_expr函數:學習

clipboard.png
clipboard.png

由於咱們是賦值運算,咱們此時走到了assign函數,接下來就是整個編譯過程當中的重點部分:ui

clipboard.png

打印var_ast->kind:spa

clipboard.png

能夠看出咱們接下來要走的就是上圖的幾個函數,那麼,這幾個函數又是怎麼走的呢?咱們接着看,首先,進入zend_delayed_compile_begin():調試

clipboard.png

這個函數的做用是取棧頂,而後在函數結束後賦值給offest,那咱們看看這個offest是什麼?

等號左邊$a的編譯

接下來進入$a的處理函數:

clipboard.png

在這裏記錄下咱們處理$a過程當中調用的函數:

zend_delayed_compile_var (result=0x7fffffffa580, ast=0x7ffff5e7f060, type=1)
zend_compile_simple_var (result=0x7fffffffa580, ast=0x7ffff5e7f060, type=1, delayed=1)
zend_try_compile_cv (result=0x7fffffffa580, ast=0x7ffff5e7f060)
lookup_cv (op_array=0x7ffff5e75460, name=0x7ffff5e5e4e0)

lookup_cv函數的做用是什麼呢?首先咱們先看lookcv的源代碼:

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

咱們發現,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數組,節省了分配編號的時間。
另外,在zend_try_compile_cv這個函數中對於result進行賦值,那麼咱們打印下result的值:

clipboard.png

咱們發現,op_type=16(IS_CV),u.op.var=80
到此咱們總結一下$a這個過程,核心函數lookupcv,在lookupcv中咱們將變量存儲在op_array->vars中,而且返回一個int型整數,表明着偏移量。隨後在zend_try_compile_cv中將op_type和u.op.var賦值給znode *result,具體編譯示例圖以下圖所示:

clipboard.png

到此,$a即左子節點就結束了。

等號右邊1的編譯

接下來咱們來進行右子樹的處理,gdb如圖:

clipboard.png

右子樹的處理比較簡單,其調用函數爲:

zend_compile_expr
ZVAL_COPY(z, v)

重點函數在於ZVAL_COPY這個宏,首先咱們看gdb中到達了這個宏:

clipboard.png

而後咱們繼續分析這個宏的做用,老規矩,先貼源碼:

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

它的功能是把一個zval(v)拷貝到另一個zval(z)中,具體的一些分析請查看上一篇文章:【PHP源碼學習】2019-03-22 AST的遍歷-1
在進行復制完以後咱們對於result進行打印:

clipboard.png

咱們能夠看到已經完成了賦值。
最後進行了 result->op_type = IS_CONST,op_type的賦值:

clipboard.png

從新打印result即最終的結果:
clipboard.png
至此,$a和1都分別存在了兩個znode中。下邊開始生成指令。

根據assign以及op1,op2生成opline

咱們進入到zend_emit_op函數中,這個函數中會生成opcode:

clipboard.png

首先咱們看這個函數所執行的全部指令:

clipboard.png

其中,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)

從代碼中能夠看出,對於操做數1,會將編譯過程當中臨時的結構znode傳遞給zend_op中,對於操做數2,由於是常量(IS_CONST),會調用zend_add_literal將其插入到op_array->literals中。
接下來咱們進行返回值的設置,此時會調用zend_make_var_result這個函數:

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位置
    GET_NODE(result, opline->result);
}
static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */
{
      return (uint32_t)op_array->T++;
}

返回值的類型爲IS_VAR,result.var爲T的值
最後打印opline看一下最終的結果:

clipboard.png

,下面咱們給出Assign操做對應的指令圖,如圖所示:

clipboard.png

從圖中能夠看出,生成的opline中opcode等於38;op1的類型爲IS_CV,op1.var對應的是vm_stack上的偏移量;op2的類型爲IS_CONST,op2.constant對應的是op_array中literals數組的下標;result的類型爲IS_VAR,result.var對應的是T的值;此時handler的值爲空。
到此,編譯階段告一段落。

參考資料:

相關文章
相關標籤/搜索