前言 這一次,我圍繞Hello World來展開Zend虛擬機器的執行過程。Hello World的PHP版本: <?php echo 'Hello World'; ?> 前一篇文章聊到的詞法分析階段就會把上邊的指令碼分析出一個Token序列: 我們得到一個Token序列:T_OPEN_TAG, T_ECHO, T_CONSTANT_ENCAPSED_STRING, ';', T_CLOSE_TAG。但在Zend虛擬機器執行的過程中,是怎麼去分析這個Token序列的。
跟蹤運行軌跡 我們還是從命令列入手,在$PHPSRC/sapi/cli/php_cli.c中的do_cli函數裡邊接收了命令列的參數輸入(php -f HelloWorld.php表示執行HelloWorld.php檔案)。 我們追蹤到$PHPSRC/main/main.c裡邊有php_execute_script的定義,緊接著調用了zend_execute_scripts() <Zend/Zend.c>,在zend_execute_scripts的定義裡邊我們發現了: EG(active_op_array) =
zend_compile_file(file_handle, type TSRMLS_CC);
zend_execute(EG(active_op_array) TSRMLS_CC); 首先通過zend_compile_file把檔案解析成opcode中間代碼(這一步會經過詞法文法分析),然後用zend_execute執行這個產生的中間代碼(這裡就是所謂的運行時)。 這裡很像C語言的編譯方式,先編譯成彙編,然後再轉成機器碼,這裡的opcode就類似C語言編譯過程中產生的彙編。 還可以延伸出一個思路,因為每次解析PHP檔案時,都需要經過詞法文法分析得到對應的opcode,其實在指令檔不變化的時候,產生的opcode也不需要變化,因此為了減少PHP指令碼的執行時間,可以把指令碼的opcode緩衝起來(例如緩衝在共用記憶體裡邊)。 我給出一個流程圖,然後隨著這個流程圖,看看Zend做了些什麼事情: 我們先看看如何編譯出opcode的。
詞法文法分析->opcode 從上節知道我們通過zend_compile_file(實際上為compile_file()<定義在Zend/zend_language_scanner.c的555行>)把指令檔編譯出opcode,實際上通過zendparse這個API來編譯出opcode的。 PHP的文法解析器是用bison來產生,安裝完之後在$PHPSRC/Zend目錄運行: bison -o zend_language_parser.c zend_language_parser.y 在Zend目錄下就會產生文法解析器zend_language_parser.c。而這裡的zendparse就是文法解析器裡邊的yyparse! 我們忽略掉產生的文法解析器,就Hello World的例子來跟蹤一下bison的聲明檔案(我去掉不想關的聲明):
start:top_statement_list { zend_do_end_compilation(TSRMLS_C); };top_statement_list:top_statement_list { zend_do_extended_info(TSRMLS_C); } top_statement { HANDLE_INTERACTIVE(); }| /* empty */;top_statement:statement { zend_verify_namespace(TSRMLS_C); };statement:unticked_statement { DO_TICKS(); }| T_STRING ':' { zend_do_label(&$1 TSRMLS_CC); };unticked_statement:| T_ECHO echo_expr_list ';'echo_expr_list:echo_expr_list ',' expr { zend_do_echo(&$3 TSRMLS_CC); }| expr { zend_do_echo(&$1 TSRMLS_CC); };expr:r_variable { $$ = $1; }| expr_without_variable { $$ = $1; };expr_without_variable:| scalar { $$ = $1; }scalar:| common_scalar { $$ = $1; };common_scalar:| T_CONSTANT_ENCAPSED_STRING { $$ = $1; };
文法分析從start開始,自上而下的分析,一個PHP指令碼就是對應一個top_statement_list,接著分成每一行一條語句statement,發現echo 'Hello World'是一條unticked_statement(留意一下echo_expr_list的聲明, 我們還可以發現文法上是支援echo 'Hello', ' World'的)。最後遞迴到T_CONSTANT_ENCAPSED_STRING狀態就結束了這一行的文法解析。在這裡我們忽略掉編譯原理在文法分析階段是怎麼去做回溯等等東西,我們關注一下Zend引擎自身的的問題。 在規則後邊的塊"{}"裡邊的代碼就是用來處理掃描到此規則時的動作,可以看到echo的執行是調用了zend_do_echo函數的。在動作聲明的塊裡邊我們看到了$$, $1,$2,$3等,這些對應的就是該條規則裡邊的傳回值,參數1,參數2……,這裡的傳回值以及參數都是YYSTYPE類型,這個類型在43行裡邊有定義:#define YYSTYPE znode。znode的定義在zend_compile.h裡邊: 留意到zend_op這個結構,於是跟蹤發現這個就是最後每條語句對應的opcode結構了。。。。 opcode的結構跟彙編有很大的相似之處,一個操作符,兩個運算元。 在Zend引擎中,每個opcode主要的東西就是那個handler,一會我們會看到Zend裡邊是怎麼產生這個handler的。到了這裡先Hold住一下,回過頭,我們看一下Hello World這個例子產生的opcode是什麼。 裝上vld,然後運行:php -dvld.active=1 HelloWorld.php,我們就可以看到這個PHP檔案編譯出來的opcode列表了: 可以看到echo這個語句的opcode類型是ECHO,同時return沒有傳回值,只有一個運算元"Hello World"。 現在經過了文法分析,我們對每條語句都編譯出了opcode,Zend就會把它放入一個op_array裡邊(其實就是一個opcode的列表)。 回過頭我們看一下zend_do_echo做了什麼事情: 首先通過get_next_op在當前的op_array的最後邊產生一條opcode,然後設定其opcode類型是ZEND_ECHO,然後設定它的第一個參數op1,同時標記第二個參數op2是不需要使用的(unused的)。 經過了這麼多步驟之後我們得到了一個op_array的列表,這個列表裡邊的每一條opcode都綁定了自己的類型,接著我們看一下每個opcode節點是如何綁定handler的。 zend_vm_def.h定義了ZEND_ECHO的handler,留意到這裡的40,一會需要用到,因為echo的參數可以有幾種:常量,變數等等,所以對應著不同的handler 在zend_vm_execute.h定義了opcode對應的所有的handler,我們只關注echo相關的handler,注意到其中的代碼: void zend_init_opcodes_handlers(void){static const opcode_handler_t labels[] = {//40913行ZEND_ECHO_SPEC_CONST_HANDLER,//41914行ZEND_ECHO_SPEC_CONST_HANDLER,ZEND_ECHO_SPEC_CONST_HANDLER,ZEND_ECHO_SPEC_CONST_HANDLER,ZEND_ECHO_SPEC_CONST_HANDLER}; 請花費短暫的時間先記住這裡的labels以及行數。 發現了擷取handler的方法最後邊return語句的計算,根據前面說的echo的opcode是40(假設兩個參數op1,op2的type都是0),於是乎其對應的handler就是: zend_opcode_handlers[40*25+0*5+0*5] = zend_opcode_handlers[1000] = labels[1000] = ZEND_ECHO_SPEC_CONST_HANDLER(怎麼來的。因為:41914行-40913行-1=1000)。
虛擬機器執行opcode 前邊我們已經解釋了zend_compile_file把一個指令碼編譯成一個opcode的list: EG(active_op_array) =
zend_compile_file(file_handle, type TSRMLS_CC);
zend_execute(EG(active_op_array) TSRMLS_CC); 在這之後,Zend引擎用zend_execute執行返回的opcode。 我們定位到了zend_execute最後執行到Zend/zend_vm_execute.h的337行: 可以看到,虛擬機器執行的時候會迴圈當前的opcode列表,然後調用每一行opcode的handler,根據handler傳回值確定下一步做啥(例如函數調用等,以後再展開)。 在這篇文章中我們只關注跟Hello World相關的東西,我們前邊知道echo的handler是ZEND_ECHO_SPEC_CONST_HANDLER,通過最後的定位你會發現其調用了: zend_write = (zend_write_func_t) utility_functions->write_function; 這裡的utility_functions裡邊包含了一些基礎的handler,每個sapi接入層自己修改了這裡的基礎函數指標,例如在命令列模式下,最後調用到了 sapi_cli_single_write: 從源碼中,我們看到了最後的寫操作就是調用了write/fwrite寫入到標準輸出資料流(也即是終端螢幕上)。
結語 最後根據前邊的過程,再展開一下流程圖就是: