<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                ### 3.1.2 抽象語法樹編譯流程 上一小節我們簡單介紹了從PHP代碼解析為抽象語法樹的過程,這一節我們再介紹下從 __抽象語法樹->Opcodes__ 的過程。 語法解析過程的產物保存于CG(AST),接著zend引擎會把AST進一步編譯為 __zend_op_array__ ,它是編譯階段最終的產物,也是執行階段的輸入,后面我們介紹的東西基本都是圍繞zend_op_array展開的,AST解析過程確定了當前腳本定義了哪些變量,并為這些變量 __順序編號__ ,這些值在使用時都是按照這個編號獲取的,另外也將變量的初始化值、調用的函數/類/常量名稱等值(稱之為字面量)保存到zend_op_array.literals中,這些字面量也有一個唯一的編號,所以執行的過程實際就是根據各指令調用不同的C函數,然后根據變量、字面量、臨時變量的編號對這些值進行處理加工。 我們首先看下zend_op_array的結構,明確幾個關鍵信息,然后再看下ast編譯為zend_op_array的過程。 #### 3.1.2.1 zend_op_array數據結構 PHP主腳本會生成一個zend_op_array,每個function也會編譯為獨立的zend_op_array,所以從二進制程序的角度看zend_op_array包含著當前作用域下的所有堆棧信息,函數調用實際就是不同zend_op_array間的切換。 ![](https://box.kancloud.cn/dfec6aa0539ecfc2143504bccc1bffa0_788x332.png) ```c 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]; }; ``` zend_op_array.opcodes指向指令列表,具體每條指令的結構如下: ```c struct _zend_op { const void *handler; //指令執行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; //返回值類型 }; //操作數結構 typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; /* Needs to be signed */ uint32_t jmp_offset; } znode_op; ``` opcode各字段含義下面展開說明。 ##### 3.1.2.1.1 handler handler為每條opcode對應的C語言編寫的 __處理過程__ ,所有opcode對應的處理過程定義在`zend_vm_def.h`中,值得注意的是這個文件并不是編譯時用到的,因為opcode的 __處理過程__ 有三種不同的提供形式:CALL、SWITCH、GOTO,默認方式為CALL,這個是什么意思呢? 每個opcode都代表了一些特定的處理操作,這個東西怎么提供呢?一種是把每種opcode負責的工作封裝成一個function,然后執行器循環執行即可,這就是CALL模式的工作方式;另外一種是把所有opcode的處理方式通過C語言里面的label標簽區分開,然后執行器執行的時候goto到相應的位置處理,這就是GOTO模式的工作方式;最后還有一種方式是把所有的處理方式寫到一個switch下,然后通過case不同的opcode執行具體的操作,這就是SWITCH模式的工作方式。 假設opcode數組是這個樣子: ```c int op_array[] = { opcode_1, opcode_2, opcode_3, ... }; ``` 各模式下的工作過程類似這樣: ```c //CALL模式 void opcode_1_handler() {...} void opcode_2_handler() {...} ... void execute(int []op_array) { void *opcode_handler_list[] = {&opcode_1_handler, &opcode_2_handler, ...}; while(1){ void handler = opcode_handler_list[op_array[i]]; handler(); //call handler i++; } } //GOTO模式 void execute(int []op_array) { while(1){ goto opcode_xx_handler_label; } opcode_1_handler_label: ... opcode_2_handler_label: ... ... } //SWITCH模式 void execute(int []op_array) { while(1){ switch(op_array[i]){ case opcode_1: ... case opcode_2: ... ... } i++; } } ``` 三種模式效率是不同的,GOTO最快,怎么選擇其它模式呢?下載PHP源碼后不要直接編譯,Zend目錄下有個文件:`zend_vm_gen.php`,在編譯PHP前執行:`php zend_vm_gen.php --with-vm-kind=CALL|SWITCH|GOTO`,這個腳本將重新生成:`zend_vm_opcodes.h`、`zend_vm_opcodes.c`、`zend_vm_execute.h`三個文件覆蓋原來的,然后再編譯PHP即可。 后面分析的過程使用的都是默認模式`CALL`,也就是opcode對應的handler為一個函數指針,編譯時opcode對應的handler是如何根據opcode索引到的呢? opcode的數值各不相同,同時可以根據兩個zend_op的類型設置不同的處理handler,因此每個opcode指令最多有20個(25去掉重復的5個)對應的處理handler,所有的handler按照opcode數值的順序定義在一個大數組中:`zend_opcode_handlers`,每25個為同一個opcode,如果對應的op_type類型handler則可以設置為空: ```c //zend_vm_execute.h void zend_init_opcodes_handlers(void) { static const void *labels[] = { ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ... }; zend_opcode_handlers = labels; } ``` 索引的算法: ```c //zend_vm_execute.h static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const zend_op* op) { //因為op_type為2的倍數,所以這里做了下轉化,轉成了0-4 static const int zend_vm_decode[] = { _UNUSED_CODE, /* 0 */ _CONST_CODE, /* 1 = IS_CONST */ _TMP_CODE, /* 2 = IS_TMP_VAR */ _UNUSED_CODE, /* 3 */ _VAR_CODE, /* 4 = IS_VAR */ _UNUSED_CODE, /* 5 */ _UNUSED_CODE, /* 6 */ _UNUSED_CODE, /* 7 */ _UNUSED_CODE, /* 8 = IS_UNUSED */ _UNUSED_CODE, /* 9 */ _UNUSED_CODE, /* 10 */ _UNUSED_CODE, /* 11 */ _UNUSED_CODE, /* 12 */ _UNUSED_CODE, /* 13 */ _UNUSED_CODE, /* 14 */ _UNUSED_CODE, /* 15 */ _CV_CODE /* 16 = IS_CV */ }; //根據op1_type、op2_type、opcode得到對應的handler return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]]; } ZEND_API void zend_vm_set_opcode_handler(zend_op* op) { //設置zend_op的handler,這個操作是在編譯期間完成的 op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op); } #define _CONST_CODE 0 #define _TMP_CODE 1 #define _VAR_CODE 2 #define _UNUSED_CODE 3 #define _CV_CODE 4 ``` ##### 3.1.2.1.2 操作數(znode_op) 操作數類型實際就是個32位整形,它主要用于存儲一些變量的索引位置、數值記錄等等。 ```c typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; /* Needs to be signed */ uint32_t jmp_offset; } znode_op; ``` 每條opcode都有兩個操作數(不一定都用到),操作數記錄著當前指令的關鍵信息,可以用于變量的存儲、訪問,比如賦值語句:"$a = 45;",兩個操作數分別記錄"$a"、"45"的存儲位置,執行時根據op2取到值"45",然后賦值給"$a",而"$a"的位置通過op1獲取到。當然操作數并不是全部這么用的,上面只是賦值時候的情況,其它操作會有不同的用法,如函數調用時的傳參,op1記錄的就是傳遞的參數是第幾個,op2記錄的是參數的存儲位置,result記錄的是函數接收參數的存儲位置。 ##### 3.1.2.1.3 操作數類型(op_type) 每個操作都有5種不同的類型: ```c #define IS_CONST (1<<0) //1 #define IS_TMP_VAR (1<<1) //2 #define IS_VAR (1<<2) //4 #define IS_UNUSED (1<<3) //8 #define IS_CV (1<<4) //16 ``` * IS_CONST:字面量,編譯時就可確定且不會改變的值,比如:$a = "hello~",其中字符串"hello~"就是常量 * IS_TMP_VAR:臨時變量,比如:$a = "hello~" . time(),其中`"hello~" . time()`的值類型就是IS_TMP_VAR,再比如:$a = "123" + $b,`"123" + $b`的結果類型也是IS_TMP_VAR,從這兩個例子可以猜測,臨時變量多是執行期間其它類型組合現生成的一個中間值,由于它是現生成的,所以把IS_TMP_VAR賦值給IS_CV變量時不會增加其引用計數 * IS_VAR:PHP變量,這個很容易認為是PHP腳本里的變量,其實不是,這里PHP變量的含義可以這樣理解:PHP變量是沒有顯式的在PHP腳本中定義的,不是直接在代碼通過`$var_name`定義的。這個類型最常見的例子是PHP函數的返回值,再如`$a[0]`數組這種,它取出的值也是`IS_VAR`,再比如`$$a`這種 * IS_UNUSED:表示操作數沒有用 * IS_CV:PHP腳本變量,即腳本里通過`$var_name`定義的變量,這些變量是編譯階段確定的,所以是compile variable, `result_type`除了上面幾種類型外還有一種類型`EXT_TYPE_UNUSED (1<<5)`,返回值沒有使用時會用到,這個跟`IS_UNUSED`的區別是:`IS_UNUSED`表示本操作返回值沒有意義(也可簡單的認為沒有返回值),而`EXT_TYPE_UNUSED`的含義是有返回值,但是沒有用到,比如函數返回值沒有接收。 ##### 3.1.2.1.4 字面量、變量的存儲 我們先想一下C程序是如何讀寫字面量、變量的。 ```c #include <stdio.h> int main() { char *name = "pangudashu"; printf("%s\n", name); return 0; } ``` 我們知道指針name分配在棧上,而"pangudashu"分配在常量區,那么"name"變量名分配在哪呢? 實際上C里面是不會存變量名稱的,編譯的過程會將變量名替換為偏移量表示:`ebp - 偏移量`或`esp + 偏移量`,將上面的代碼轉為匯編: ```c .LC0: .string "pangudashu" .text .globl main .type main, @function main: .LFB0: pushq %rbp movq %rsp, %rbp subq $16, %rsp movq $.LC0, -8(%rbp) movq -8(%rbp), %rax movq %rax, %rdi call puts movl $0, %eax leave ``` 可以看到`movq $.LC0, -8(%rbp)`,而`-8(%rbp)`就是name變量。 雖然PHP代碼不會直接編譯為機器碼,但編譯、執行的設計跟C程序是一致的,也有常量區、變量也通過偏移量訪問、也有虛擬的執行棧。 ![](https://box.kancloud.cn/61267676e019477ccf98587e67a8dfb4_449x359.png) 在編譯時就可確定且不會改變的量稱為字面量,也稱作常量(IS_CONST),這些值在編譯階段就已經分配zval,保存在`zend_op_array->literals`數組中(對應c程序的常量存儲區),訪問時通過`_zend_op_array->literals + 偏移量`讀取,舉個例子: ```c <?php $a = 56; $b = "hello"; ``` `56`通過`(zval*)(_zend_op_array->literals + 0)`取到,`hello`通過`(zval*)(_zend_op_array->literals + 16)`取到,具體變量的讀寫操作將在執行階段詳細分析,這里只分析編譯階段的操作。 #### 3.1.2.2 AST->zend_op_array 上面我們介紹了zend_op_array結構,接下來我們回過頭去看下語法解析(zendparse())之后的流程: ```c 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()__ 中完成,這個函數是總入口,會被多次遞歸調用: ```c //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`編譯當前節點: ```c 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)作不同的處理,我們不會把每種類型的處理都講一遍,這里還是根據上一節最后的例子挑幾個看下具體的處理過程。 ```php $a = 123; $b = "hi~"; echo $a,$b; ``` zendparse()階段生成的AST: ![](https://box.kancloud.cn/3989dc29734e977a9e49740f78384647_1098x493.png) 下面的過程比較復雜,有的函數會多次遞歸調用,我們根據例子一步步去看下,如果你對PHP各個語法實現比較熟悉再去看整個AST的編譯過程就會比較輕松。 > __(1)、__ 首先從根節點開始,有3個child,第一個節點類型為ZEND_AST_ASSIGN,zend_compile_stmt()中走到default分支 > __(2)、__ ZEND_AST_ASSIGN類型由zend_compile_expr()處理: ```c void zend_compile_expr(znode *result, zend_ast *ast) { CG(zend_lineno) = zend_ast_get_lineno(ast); switch (ast->kind) { case ZEND_AST_ZVAL: ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); result->op_type = IS_CONST; return; case ZEND_AST_VAR: zend_compile_var(result, ast, BP_VAR_R); return; case ZEND_AST_ASSIGN: zend_compile_assign(result, ast); return; ... } } ``` > 繼續進入zend_compile_assign(): ```c void zend_compile_assign(znode *result, zend_ast *ast) { zend_ast *var_ast = ast->child[0]; //變量名 zend_ast *expr_ast = ast->child[1];//變量值表達式 znode var_node, expr_node; zend_op *opline; uint32_t offset; if (is_this_fetch(var_ast)) { //檢查變量名是否為this,變量名不能是this zend_error_noreturn(E_COMPILE_ERROR, "Cannot re-assign $this"); } //比如這樣寫:my_function() = 123;即:將函數的返回值作為變量名將報錯 zend_ensure_writable_variable(var_ast); 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); //生成變量名的znode,這個結構只在這個地方臨時用,所以直接分配在stack上 zend_compile_expr(&expr_node, expr_ast); //遞歸編譯變量值表達式,最終需要得到一個ZEND_AST_ZVAL的節點 zend_delayed_compile_end(offset); zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); //生成一條op return; ... } } ``` > 這個地方主要有三步關鍵操作: >> __第1步:__ 變量賦值操作有兩部分:變量名、變量值,所以首先是針對變量名的操作,介紹zend_op_array時曾提到每個PHP變量都有一個編號,變量的讀寫都是根據這個編號操作的,這個編號最早就是這一步生成的。 ![](https://box.kancloud.cn/fb367323f2bb710074b81692c24bfd8c_658x470.png) >> 中間過程我們不再細看,這里重點看下變量編號的過程,這個過程比較簡單,每發現一個變量就遍歷zend_op_array.vars數組,看此變量是否已經保存,沒有保存的話則存入vars,然后后續變量的使用都是用的這個變量在數組中的下標,比如第一次定義的時候:`$a = 123;`將$a編號為0,然后:`echo $a;`再次使用時會遍歷vars,發現已經存在,直接用其下標操作$a。 ```c static int lookup_cv(zend_op_array *op_array, zend_string* name) { int i = 0; zend_ulong hash_value = zend_string_hash_val(name); //遍歷op_array.vars檢查此變量是否已存在 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*));//擴容vars } op_array->vars[i] = zend_new_interned_string(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); //傳NULL時返回的是96 + i*sizeof(zval) } ``` >> __注意:這里變量的編號從0、1、2、3...依次遞增的,但是實際使用中并不是直接用的這個下標,而是轉化成了內存偏移量offset,這個是`ZEND_CALL_VAR_NUM`宏處理的,所以變量偏移量實際是96、112、128...遞增的,這個96是根據zend_execute_data大小設定的(不同的平臺下對應的值可能不同),下一篇介紹zend執行流程時會詳細介紹這個結構。__ ```c #define ZEND_CALL_FRAME_SLOT \ ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval)))) #define ZEND_CALL_VAR_NUM(call, n) \ (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n)))) ``` >> __第2步:__ 編譯變量值表達式,再次調用zend_compile_expr()編譯,示例中的情況比較簡單,expr_ast.kind為ZEND_AST_ZVAL: ```c void zend_compile_expr(znode *result, zend_ast *ast) { switch (ast->kind) { case ZEND_AST_ZVAL: ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); //將變量值復制到znode.u.constant中 result->op_type = IS_CONST; //類型為IS_CONST,這種value后面將會保存在zend_op_array.literals中 return; ... } } ``` >> __第3步:__ 上面兩步已經分別生成了變量賦值的op1、op2,下面就是根據這倆值生成opcode的過程。 ```c 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)); //當前zend_op_array下生成一條新的指令 opline->opcode = opcode; //將op1、op2內容拷貝到zend_op中,設置op_type //如果znode.op_type == IS_CONST,則會將znode.u.contstant值轉移到zend_op_array.literals中 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); } //如果此指令有返回值則想變量那樣為返回值編號(后面分配局部變量時將根據這個編號索引) if (result) { zend_make_var_result(result, opline); } return opline; } 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上,上面介紹zend_op_array時說過T、last_var的區別 GET_NODE(result, opline->result); } ``` >> 到這我們示例中的第1條賦值語句就算編譯完了,第2條同樣是賦值,過程與上面相同,我們直接看最好一條輸出的語句。 > __(3)、__ echo語句的編譯:`echo $a,$b;`實際從編譯后的語法樹就可以看出,一次echo多個也被編譯為多次echo了,所以示例中的用法與:`echo $a; echo $b;`等價,我們只分析其中一個就可以了。 ![](https://box.kancloud.cn/f038636af0b6e51137e23a26c86c5e23_405x386.png) > `zend_compile_stmt()`中首先發現節點類型是`ZEND_AST_STMT_LIST`,然后調用`zend_compile_stmt_list()`分別編譯child,具體的流程如下圖所示: ![](https://box.kancloud.cn/ac902f5ee528164501de47aa1759e4eb_838x656.png) > 最后生成`zend_op`的過程: ```c void zend_compile_echo(zend_ast *ast) { zend_op *opline; zend_ast *expr_ast = ast->child[0]; znode expr_node; zend_compile_expr(&expr_node, expr_ast); opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);//生成1條新的opcode opline->extended_value = 0; } ``` 最終`zend_compile_top_stmt()`編譯完成后整個編譯流程基本是完成了,`CG(active_op_array)`結構如下圖所示,但是后面還有一個處理`pass_two()`。 ![](https://box.kancloud.cn/7776e4560a65af88dadff76920b2a882_1093x442.png) ```c ZEND_API int pass_two(zend_op_array *op_array) { zend_op *opline, *end; if (!ZEND_USER_CODE(op_array->type)) { return 0; } //重置一些CG(context)的值,暫且忽略 ... opline = op_array->opcodes; end = opline + op_array->last; while (opline < end) { switch(opline->opcode){ //這里對一些操作進行針對性的處理,后面有遇到的情況我們再看 ... } //如果是IS_CONST會將數組下標轉化為內存偏移量,與IS_CV那種處理方式相同 //所以這里實際就是將0、1、2...轉為為16、32、48...(即:編號*sizeof(zval)) if (opline->op1_type == IS_CONST) { ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1); } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) { //上面作相同的處理,不同的是這里的起始值是接著IS_CV的 opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var); } //與op1完全相同 if (opline->op2_type == IS_CONST) { ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2); } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) { opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var); } //返回值與op1/2相同處理 if (opline->result_type & (IS_VAR|IS_TMP_VAR)) { opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var); } //設置此opcode的處理handler ZEND_VM_SET_OPCODE_HANDLER(opline); opline++; } //標識當前op_array已執行過此操作 op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO; return 0; } ``` 拋開特殊opcode的處理,`pass_two()`主要有兩個重要操作: * (1)將IS_CONST、IS_VAR、IS_TMP_VAR類型的操作數、返回值轉化為內存偏移量,與上面提到的IS_CV變量的處理一樣,其中IS_CONST類型起始值為0,然后按照編號依次遞增sizeof(zval),而IS_VAR、IS_TMP_VAR唯一的不同時它的初始值接著IS_CV的,簡單的講就是先安排PHP變量的,然后接著才是各條語句的中間值、返回值 * (2)另外一個重要操作就是設置各指令的處理handler,這個前面《3.1.2.1.1 handler》已經介紹過其索引規則 經過`pass_two()`處理后opcodes的樣子: ![](https://box.kancloud.cn/0b5831bee386913c3a760be49585d821_501x367.png) __總結:__ 到這里整個PHP編譯階段就算全部完成了,最終編譯的結果就是zend_op_array,其中最核心的操作就是AST的編譯了,有興趣的可以多寫幾個例子去看下不同節點類型的處理方式。 另外,編譯階段很關鍵的一個操作就是確定了各個 __變量、中間值、臨時值、返回值、字面量__ 的 __內存編號__ ,這個地方非常重要,后面介紹執行流程時也會用到。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看