【轉】中間代碼opcode的執行

原文連接:http://www.orlion.ga/941/php

原文:http://www.nowamagic.net/librarys/veda/detail/1543數組

    

假如咱們如今使用的是CLI模式,直接在SAPI/cli/php_cli.c文件中找到main函數, 默認狀況下PHP的CLI模式的行爲模式爲PHP_MODE_STANDARD。 此行爲模式中PHP內核會調用php_execute_script(&file_handle TSRMLS_CC);來執行PHP文件。 順着這條執行的線路,能夠看到一個PHP文件在通過詞法分析,語法分析,編譯後生成中間代碼的過程:緩存

1 EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC);

在銷燬了文件所在的handler後,若是存在中間代碼,則PHP虛擬機將經過如下代碼執行中間代碼:函數

1 zend_execute(EG(active_op_array) TSRMLS_CC);

若是你是使用VS查看源碼的話,將光標移到zend_execute並直接按F12, 你會發現zend_execute的定義跳轉到了一個指針函數的聲明(Zend/zend_execute_API.c)。優化

1 ZEND_API void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);

這是一個全局的函數指針,它的做用就是執行PHP代碼文件解析完的轉成的zend_op_array。 和zend_execute相同的還有一個zedn_execute_internal函數,它用來執行內部函數。 在PHP內核啓動時(zend_startup)時,這個全局函數指針將會指向execute函數。 注意函數指針前面的修飾符ZEND_API,這是ZendAPI的一部分。 在zend_execute函數指針賦值時,還有PHP的中間代碼編譯函數zend_compile_file(文件形式)和zend_compile_string(字符串形式)。spa

1 zend_compile_file = compile_file;
2 zend_compile_string = compile_string;
3 zend_execute = execute;
4 zend_execute_internal = NULL;
5 zend_throw_exception_hook = NULL;

這幾個全局的函數指針均只調用了系統默認實現的幾個函數,好比compile_file和compile_string函數, 他們都是以全局函數指針存在,這種實現方式在PHP內核中比比皆是,其優點在於更低的耦合度,甚至能夠定製這些函數。 好比在APC等opcode優化擴展中就是經過替換系統默認的zend_compile_file函數指針爲本身的函數指針my_compile_file, 而且在my_compile_file中增長緩存等功能。.net

到這裏咱們找到了中間代碼執行的最終函數:execute(Zend/zend_vm_execure.h)。 在這個函數中全部的中間代碼的執行最終都會調用handler。這個handler是什麼呢?指針

1 if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
2 }

這裏的handler是一個函數指針,它指向執行該opcode時調用的處理函數。 此時咱們須要看看handler函數指針是如何被設置的。 在前面咱們有提到和execute一塊兒設置的全局指針函數:zend_compile_string。 它的做用是編譯字符串爲中間代碼。在Zend/zend_language_scanner.c文件中有compile_string函數的實現。 在此函數中,當解析完中間代碼後,通常狀況下,它會執行pass_two(Zend/zend_opcode.c)函數。 pass_two這個函數,從其命名上真有點看不出其意義是什麼。 可是咱們關注的是在函數內部,它遍歷整個中間代碼集合, 調用ZEND_VM_SET_OPCODE_HANDLER(opline);爲每一箇中間代碼設置處理函數。 ZEND_VM_SET_OPCODE_HANDLER是zend_vm_set_opcode_handler函數的接口宏, zend_vm_set_opcode_handler函數定義在Zend/zend_vm_execute.h文件。 其代碼以下:code

01 static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
02 {
03         static const int zend_vm_decode[] = {
04             _UNUSED_CODE, /* 0              */
05             _CONST_CODE,  /* 1 = IS_CONST   */
06             _TMP_CODE,    /* 2 = IS_TMP_VAR */
07             _UNUSED_CODE, /* 3              */
08             _VAR_CODE,    /* 4 = IS_VAR     */
09             _UNUSED_CODE, /* 5              */
10             _UNUSED_CODE, /* 6              */
11             _UNUSED_CODE, /* 7              */
12             _UNUSED_CODE, /* 8 = IS_UNUSED  */
13             _UNUSED_CODE, /* 9              */
14             _UNUSED_CODE, /* 10             */
15             _UNUSED_CODE, /* 11             */
16             _UNUSED_CODE, /* 12             */
17             _UNUSED_CODE, /* 13             */
18             _UNUSED_CODE, /* 14             */
19             _UNUSED_CODE, /* 15             */
20             _CV_CODE      /* 16 = IS_CV     */
21         };
22         return zend_opcode_handlers[opcode * 25
23                 + zend_vm_decode[op->op1.op_type] * 5
24                 + zend_vm_decode[op->op2.op_type]];
25 }
26  
27 ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
28 {
29     op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
30 }

前面介紹了四種查找opcode處理函數的方法, 而根據其本質實現查找也在其中,只是這種方法對於計算機來講比較容易識別,而對於天然人來講卻不太友好。 好比一個簡單的A + B的加法運算,若是你想用這種方法查找其中間代碼的實現位置的話, 首先你須要知道中間代碼的表明的值,而後知道第一個表達式和第二個表達式結果的類型所表明的值, 而後計算獲得一個數值的結果,而後從數組zend_opcode_handlers找這個位置,位置所在的函數就是中間代碼的函數。 這對閱讀代碼的速度沒有好處,可是在開始閱讀代碼的時候根據代碼的邏輯走這樣一個流程倒是大有好處。遞歸

回到正題。 handler所指向的方法基本都存在於Zend/zend_vm_execute.h文件文件。 知道了handler的由來,咱們就知道每一個opcode調用handler指針函數時最終調用的位置。

在opcode的處理函數執行完它的本職工做後,常規的opcode都會在函數的最後面添加一句:ZEND_VM_NEXT_OPCODE();。 這是一個宏,它的做用是將當前的opcode指針指向下一條opcode,而且返回0。以下代碼:

1 #define ZEND_VM_NEXT_OPCODE() \
2 CHECK_SYMBOL_TABLES() \
3 EX(opline)++; \
4 ZEND_VM_CONTINUE()
5  
6 #define ZEND_VM_CONTINUE()   return 0

在execute函數中,處理函數的執行是在一個while(1)循環做用範圍中。以下:

01 while (1) {
02         int ret;
03 #ifdef ZEND_WIN32
04         if (EG(timed_out)) {
05             zend_timeout(0);
06         }
07 #endif
08  
09         if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
10             switch (ret) {
11                 case 1:
12                     EG(in_execution) = original_in_execution;
13                     return;
14                 case 2:
15                     op_array = EG(active_op_array);
16                     goto zend_vm_enter;
17                 case 3:
18                     execute_data = EG(current_execute_data);
19                 default:
20                     break;
21             }
22         }
23  
24     }

前面說到每一箇中間代碼在執行完後都會將中間代碼的指針指向下一條指令,而且返回0。 當返回0時,while 循環中的if語句都不知足條件,從而使得中間代碼能夠繼續執行下去。 正是這個while(1)的循環使得PHP內核中的opcode能夠從第一條執行到最後一條, 固然這中間也有一些函數的跳轉或類方法的執行等。

以上是一條中間代碼的執行,那麼對於函數的遞歸調用,PHP內核是如何處理的呢? 看以下一段PHP代碼:

1 function t($c) {
2     echo $c"\n";
3     if ($c > 2) {
4             return ;
5     }
6     t($c + 1);
7 }
8 t(1);

這是一個簡單的遞歸調用函數實現,它遞歸調用了兩次,這個遞歸調用是如何進行的呢? 咱們知道函數的調用所在的中間代碼最終是調用zend_do_fcall_common_helper_SPEC(Zend/zend_vm_execute.h)。 在此函數中有以下一段:

1 if (zend_execute == execute && !EG(exception)) {
2     EX(call_opline) = opline;
3     ZEND_VM_ENTER();
4 else {
5     zend_execute(EG(active_op_array) TSRMLS_CC);
6 }

前面提到zend_execute API可能會被覆蓋,這裏就進行了簡單的判斷,若是擴展覆蓋了opcode執行函數, 則進行特殊的邏輯處理。

上一段代碼中的ZEND_VM_ENTER()定義在Zend/zend_vm_execute.h的開頭,以下:

1 #define ZEND_VM_CONTINUE()   return 0
2 #define ZEND_VM_RETURN()     return 1

3 #define ZEND_VM_ENTER()      return 2
相關文章
相關標籤/搜索