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_ex語法分析從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"?,F在經過了語法分析,我們對每條語句都編譯出了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寫入到標準輸出流(也即是終端屏幕上)。
結語
最后根據前邊的過程,再展開一下流程圖就是:
新聞熱點
疑難解答
圖片精選