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的編譯了,編譯階段很關鍵的一個操作就是確定了各個 變量、中間值、臨時值、返回值、字面量 的 內存編號 ,這個地方非常重要,后面介紹執行流程時也會用到。
