php代碼編譯的實現


1.php是解析型的高級語言,zend內核使用c語言實現,有main函數,php腳本就是輸入,內核處理后輸出結果,內核將php腳本翻譯成c程序可識別的opcode就是php的編譯。  

      c語言的編譯將c代碼編譯成機器碼,這些機器碼就是操作指令,將指令寫入二進制程序load相應的內存區(常量區 數據區 代碼區),分配運行棧,開始從代碼區依次執行。

      php編譯差不多,將php腳本解析成opcode,每條opcode就是c的stuct,對應着相應的機器指令,執行過程就是zend引擎執行這些opcode,編譯過程包括詞法分析、語法分析,就的php版本直接生成opcode,php7新增在語法分析階段生成抽象語法樹,然后生成opcode_array。

2.詞法分析、語法分析 (PHP代碼->抽象語法樹(AST))

      PHP使用re2c、bison完成這個階段的工作

      re2c: 詞法分析器,將輸入分割為一個個有意義的詞塊,稱為token

          bison: 語法分析器,確定詞法分析器分割出的token是如何彼此關聯的

3.opcode_array結構

     zend引擎會把AST進一步編譯為 zend_op_array ,它是編譯階段最終的產物,也是執行階段的輸入。 AST解析過程確定了當前腳本定義了哪些變量,並為這些變量 順序編號 ,這些值在使用時都是按照這個編號獲取的,另外也將變量的初始化值、調用的函數/類/常量名稱等值(稱之為字面量)保存到zend_op_array.literals中,這些字面量也有一個唯一的編號,所以執行的過程實際就是根據各指令調用不同的C函數,然后根據變量、字面量、臨時變量的編號對這些值進行處理加工。

     PHP主腳本會生成一個zend_op_array,每個function也會編譯為獨立的zend_op_array,所以從二進制程序的角度看zend_op_array包含着當前作用域下的所有堆棧信息,函數調用實際就是不同zend_op_array間的切換

                                

 

opcode的結構

struct _zend_op_array {
    //common是普通函數或類成員方法對應的opcodes快速訪問時使用的字段,后面分析PHP函數實現的時候會詳細講
    ...

    uint32_t *refcount;

    uint32_t this_var;

    uint32_t last;
    //opcode指令數組
    zend_op *opcodes;

    //PHP代碼里定義的變量數:op_type為IS_CV的變量,不含IS_TMP_VAR、IS_VAR的
    //編譯前此值為0,然后發現一個新變量這個值就加1
    int last_var;
    //臨時變量數:op_type為IS_TMP_VAR、IS_VAR的變量
    uint32_t T;
    //PHP變量名數組
    zend_string **vars; //這個數組在ast編譯期間配合last_var用來確定各個變量的編號,非常重要的一步操作
    ...

    //靜態變量符號表:通過static聲明的
    HashTable *static_variables;
    ...

    //字面量數量
    int last_literal; 
    //字面量(常量)數組,這些都是在PHP代碼定義的一些值
    zval *literals;

    //運行時緩存數組大小
    int  cache_size;
    //運行時緩存,主要用於緩存一些znode_op以便於快速獲取數據,后面單獨介紹這個機制
    void **run_time_cache;

    void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

     handler是每條opcode對應的C語言編寫的 處理過程,所有hadler定義在zend_vm_def.h中,有三種不同的提供形式:CALL、SWITCH、GOTO,默認方式為CALL。 

     每條opcode都有兩個操作數, 操作數記錄着當前指令的關鍵信息(操作數類型實際就是個32位整形,它主要用於存儲一些變量的索引位置、數值記錄等等)。

                每個操作都有5種不同的類型 IS_CONST:字面量,IS_TMP_VAR:臨時變量, IS_VAR:PHP變量,  IS_CV:PHP腳本變量, IS_UNUSED:表示操作數沒有用。

     PHP代碼不會直接編譯為機器碼,但編譯、執行的設計跟C程序是一致的,也有常量區、變量也通過偏移量訪問、也有虛擬的執行棧。

              在編譯時就可確定且不會改變的量稱為字面量,也稱作常量(IS_CONST),這些值在編譯階段就已經分配zval,保存在zend_op_array->literals數組中,訪問時通過_zend_op_array->literals + 偏移量讀取。

              

4.抽象語法樹(AST)編譯  (AST-> zend_op_array)

    

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
    zend_op_array *op_array = NULL; //編譯出的opcodes
    ...

    if (open_file_for_scanning(file_handle)==FAILURE) {//文件打開失敗
        ...
    } else {
        zend_bool original_in_compilation = CG(in_compilation);
        CG(in_compilation) = 1;

        CG(ast) = NULL;
        CG(ast_arena) = zend_arena_create(1024 * 32);
        if (!zendparse()) { //語法解析
            zval retval_zv;
            zend_file_context original_file_context; //保存原來的zend_file_context
            zend_oparray_context original_oparray_context; //保存原來的zend_oparray_context,編譯期間用於記錄當前zend_op_array的opcodes、vars等數組的總大小
            zend_op_array *original_active_op_array = CG(active_op_array);
            op_array = emalloc(sizeof(zend_op_array)); //分配zend_op_array結構
            init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE);//初始化op_array
            CG(active_op_array) = op_array; //將當前正在編譯op_array指向當前
            ZVAL_LONG(&retval_zv, 1);

            if (zend_ast_process) {
                zend_ast_process(CG(ast));
            }

            zend_file_context_begin(&original_file_context); //初始化CG(file_context)
            zend_oparray_context_begin(&original_oparray_context); //初始化CG(context)
            zend_compile_top_stmt(CG(ast)); //AST->zend_op_array編譯流程
            zend_emit_final_return(&retval_zv); //設置最后的返回值
            op_array->line_start = 1;
            op_array->line_end = CG(zend_lineno);
            pass_two(op_array);
            zend_oparray_context_end(&original_oparray_context);
            zend_file_context_end(&original_file_context);

            CG(active_op_array) = original_active_op_array;
        }
        ...
    }
    ...

    return op_array;
}

 

      compile_file()操作中有幾個保存原來值的操作,這是因為這個函數在PHP腳本執行中並不會只執行一次,主腳本執行時會第一次調用,而include、require也會調用,所以需要先保存當前值,然后執行完再還原回去。

  AST->zend_op_array編譯是在 zend_compile_top_stmt() 中完成,這個函數是總入口,會被多次遞歸調用:

//zend_compile.c
void zend_compile_top_stmt(zend_ast *ast)
{
    if (!ast) {
        return;
    }

    if (ast->kind == ZEND_AST_STMT_LIST) { //第一次進來一定是這種類型
        zend_ast_list *list = zend_ast_get_list(ast);
        uint32_t i;
        for (i = 0; i < list->children; ++i) {
            zend_compile_top_stmt(list->child[i]);//list各child語句相互獨立,遞歸編譯
        }
        return;
    }

    //各語句編譯入口
    zend_compile_stmt(ast);

    if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) {
        zend_verify_namespace();
    }
    //function、class兩種情況的處理,非常關鍵的一步操作,后面分析函數、類實現的章節再詳細分析
    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(); //很重要!!!
    }
}

 

首先從AST的根節點開始編譯,根節點類型為ZEND_AST_STMT_LIST,這個類型表示當前節點下有多個獨立的節點,各child都是獨立的語句生成的節點,所以依次編譯即可,直到到達有效節點位置(非ZEND_AST_STMT_LIST節點),然后調用zend_compile_stmt編譯當前節點:

void zend_compile_stmt(zend_ast *ast)
{
    CG(zend_lineno) = ast->lineno;

    switch (ast->kind) {
        case xxx:
            ...
        break;
        case ZEND_AST_ECHO:
            zend_compile_echo(ast);
            break;
        ...
        default:
        {
            znode result;
            zend_compile_expr(&result, ast);
            zend_do_free(&result);
        }
    }
    
    if (FC(declarables).ticks && !zend_is_unticked_stmt(ast)) {
        zend_emit_tick();
    }
}

根據不同的節點類型(kind)作不同的處理。

 

       最終編譯的結果就是zend_op_array,其中最核心的操作就是AST的編譯了,編譯階段很關鍵的一個操作就是確定了各個 變量、中間值、臨時值、返回值、字面量 的 內存編號 ,這個地方非常重要,后面介紹執行流程時也會用到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM