国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > PHP > 正文

PHP-Zend引擎剖析之詞法分析(一)

2019-11-06 06:12:11
字體:
來源:轉載
供稿:網友

前言

閑來研究一下php底層的Zend引擎源碼,Zend引擎是PHP腳本的虛擬機。在PHP上層有SAPI接口,負責對各個接入層的抽象,例如PHP在Apache模塊里邊的實現,Fast-CGI的實現,命令行的實現。在PHP底層便是Zend虛擬機,Zend虛擬機負責解析PHP語法的文件,上層可以在虛擬機中注冊函數/變量提供給虛擬機調用,例如從Apache分發過來的HTTP請求經過PHP的Apache SAPI接口后,便會注冊一些$_COOKIE、$_GET等全局變量,而在命令行模式下便沒有這些跟HTTP相關的全局變量。Zend引擎跟其他編譯器跟解釋器一樣,會經歷詞法分析/語法分析,語法分析后會生成op code,也就是PHP的中間代碼,最終Zend虛擬機執行的是op code。第一篇貢獻給Zend引擎的理當是詞法分析的源碼剖析。PS:分析的代碼是PHP-5.5.5的源碼包,下載地址:http://windows.php.net/downloads/releases/php-5.5.5-src.zip

詞法分析

詞法分析階段就是從輸入流里邊一個字符一個字符的掃描,識別出對應的詞素,最后把源文件轉換成為一個TOKEN序列,然后丟給語法分析器。從詞法分析階段中,詞法分析器也能檢測到源代碼里邊的一些錯誤。例如在Zend引擎的詞法分析階段就有這樣一段代碼:
          zend_error(E_COMPILE_WARNING, "Unterminated comment starting line %d", CG(zend_lineno));
當檢測到/*開頭,但是沒有*/結尾時,Zend引擎會拋出一個Waring提示,但是并不影響接下來的詞法解析,詞法分析階段一般都不會造成嚴重的解析錯誤,因為詞法分析階段的職責就是識別出Token序列而已,它并不需要知道Token跟Token之間是否具備什么聯系(那個應該是語法分析階段的職責)。在Zend引擎的詞法分析器中也會拋出致命的解析錯誤而終止詞法分析階段,如下代碼:
           zend_error_noreturn(E_COMPILE_ERROR, "Could not convert the script from the detected "                                   "encoding /"%s/" to a compatible encoding", zend_multibyte_get_encoding_name(LANG_SCNG(script_encoding)));
這個解析錯誤是因為從輸入流里邊檢測到的代碼的編碼不合法,顯然,這里是應該終止掉整個解析過程的。Zend引擎的詞法分析器re2c來生成,詞法分析的階段會涉及到各個狀態,其變量命名均為yy開頭(下文會說明)。

源碼高亮

我找了一個清晰的流程來分析怎么進入到詞法分析階段的。我們以命令行的PHP為入口來研究一下,以HelloWorld的例子來看,我們在命令行執行:php -s HelloWorld.php,結果如下:php -s是高亮源代碼的命令,所謂高亮源代碼其實就是對詞素進行一個顏色高亮,我們通過入口文件分析到在$PHPSRC/sapi/cli/php_cli.c中的do_cli函數里邊接收了命令行的參數輸入。-s的輸入對應的是高亮源碼。緊接著,便是調用了Zend引擎的代碼高亮的函數:zend_highlight。在$PHPSRC/Zend/zend_highlight.c中,我們找到了zend_highlight的定義,zend_highlight()調用的就是詞法分析器lex_scan來獲取Token,然后加入對應的顏色。到了這里,就真正進入詞法分析的流程了。

lex詞法分析器

Zend引擎的lex文件位于$PHPSRC/Zend/zend_language_scanner.l,如果你安裝了re2c,可以通過以下命令來生成c文件:
re2c -F -c -o zend_language_scanner.c zend_language_scanner.l
我們主要剖析的是zend_language_scanner.l文件。在re2c生成的詞法解析器中,我認為有兩個維度的狀態機。第一個維度是字符串的維度來維護的狀態,第二個是字符的維度來維護狀態。第二個維度的狀態機就是字符間狀態的跳轉,在這里我們忽略之。例如在Zend引擎中,當掃描到"<?php"時,Zend會將當前第一維度的狀態設置為ST_IN_SCRIPTING,表示現在我們已經進入了PHP腳本解析的狀態了。這個維度的狀態可以很方便的在lex文件中作為各種前置條件,例如在lex文件中有很多這樣的聲明:其表達的意思就是:當我們詞法解析器處于ST_IN_SCRIPTING這個狀態時,遇到"exit"這個字符串就返回一個T_EXIT的Token標志(在Zend引擎中Token的宏都是以T_開頭,其實際對應是一個數字)。你可以經常從語法錯誤提示信息中看到T_開頭的提示信息,例如在:echo "Hello" World!/n";字符串中加多了一個雙引號,運行時就會出現編譯錯誤,這里邊就有一個T_STRING的Token錯誤:
Parse error: syntax error, unexpected 'World' (T_STRING), expecting ',' or ';' in /home/raphealguo/tmp/HelloWorld.php on line 2
在詞法解析器掃描字符的過程中,需要記錄掃描過程的各個參數以及當前狀態,這些變量都是以yy開頭命名。常用到的就是:yy_state, yy_text, yyleng, yy_cursor, yy_limit各個變量的狀態掃描前后的變化示意圖。掃描echo前:

掃描echo后:

通過一個字符一個字符的掃描最終會得到一個Token序列,然后交由語法分析器去解析,接著就是剖析Zend引擎的lex文件規則是怎么寫的了。

lex文件剖析

Zend詞法解析狀態

Zend引擎在做詞法解析時會自己維護掃描過程的狀態,其實就是將yy_text等變量自己封裝一個結構體,我們可以在lex文件中看到很多SCNG的宏調用,例如:SCNG(yy_start) = YYCURSOR;定位一下#define SCNG,可以發現在lex文件的91行有這樣的宏定義:
/* Globals Macros */#define SCNG     LANG_SCNG
我們重新定位到#define LANG_SCNG在文件$PHPSRC/Zend/zend_globals_macros.h中的第56行(我們忽略52行ZTS的判斷,這是一個線程安全的宏定義):
# define LANG_SCNG(v) (language_scanner_globals.v)          //這里可以看到實際上在掃描過程中 都是調全局掃描狀態的屬性,例如SCNG(yy_start)相當于language_scanner_globals.yy_startextern ZEND_API zend_php_scanner_globals language_scanner_globals;#endif
可以看到Zend引擎維護了一個zend_php_scanner_globals的結構體(實際上在27行里邊是一個typedef的重命名,本來是叫做_zend_php_scanner_globals這個結構體),_zend_php_scanner_globals這個結構體的定義在$PHPSRC/Zend/zend_globals.h,可以看到其結構有部分跟原來lex掃描器的變量是一致的,但是它好包裝了一些堆棧,還有輸入輸出流(解析PHP文件時不一定是文件輸入流,也有可能從終端輸入的命令,所以這里包裝一個輸入輸出流是很合理的)。關鍵字Token回到lex詞法描述文件上,前邊說到詞法掃描的入口在zend_language_scanner.l的第999行int lex_scan(zval *zendlval TSRMLS_DC)里。先定義一些前置的正則匹配:對于一些無需復雜處理的關鍵字,我們掃描到對應的關鍵字,直接生成對應的Token標志即可,例如:在lex文件中可以看到很多這樣的規則聲明,<ST_IN_SCRIPTING>是指掃描到這個關鍵字的前置條件是詞法解析器要處于ST_IN_SCRIPTING這個狀態,在lex文件里邊有以下幾種方式可以設置當前的詞法解析器狀態
#define YYGETCONDITION()  SCNG(yy_state)#define YYSETCONDITION(s) SCNG(yy_state) = s#define BEGIN(state) YYSETCONDITION(STATE(state))static void _yy_push_state(int new_state TSRMLS_DC){//將當前狀態壓棧,然后重設當前狀態為新狀態zend_stack_push(&SCNG(state_stack), (void *) &YYGETCONDITION(), sizeof(int));YYSETCONDITION(new_state);}

進入PHP解析狀態

我們知道PHP是嵌入式的,只有包含在<?php ?>或者<? ?>標簽中的字符才會被執行解析,在lex文件的1732-1805行就是掃描<?php這樣起始標簽的規則聲明,源碼如下:當掃描到<?php時,在1790行設置了當前詞法解析器的狀態為ST_IN_SCRIPTING,其中HANDLE_NEWLINE是為了遞增當前的zend_lineno,這個變量是用來記錄當前解析到第幾行。最后return一個T_OPEN_TAG出去。當遇到短標簽<?=時,會先檢查全局屬性里邊的short_tags有沒有打開,沒有的話就goto到inline_char_handler去處理,inline_char_handler對應的就是掃描不在PHP標簽里邊的字符了。在1732行行定義了另外一種PHP語法打開標簽,就是:<script language="php">echo 2;</script>可以通過這個規則看出,如果在script里邊加入其他屬性就會導致這條規則失效,例如:<script language="php">echo 2;</script>就不會進行PHP語法解析了。

PHP注釋

接著我們看一下PHP里邊注釋是怎么掃描的。先找到1919行關于單行注釋的規則聲明:可以看出,PHP是支持#以及//兩種方式的單行注釋。處于ST_IN_SCRIPTING狀態下,遇到"#"|"//",變觸發了單行注釋的掃描,從當前字符開始一直掃描到流緩沖區的末尾(也即是while(YYCURSOR < YYLIMIT))。遇到/r/n以及/n時,遞增記錄當前解析的行(zend_lineno++),為了更好容錯性,PHP還兼容了//?>這樣的語法,也即是說當行注釋是不會注釋到?>的,可以從case '?'這個分支看出Zend的處理,先讓當前指針YYCURSOR--,回到?>前一個字符,然后跳出循環,這樣才不會吃掉"?>"導致后邊認不到PHP的關閉標簽。多行注釋的規則稍微復雜那么一點點:首先可以看到/**是對應PHP文檔聲明的解析(在文檔中是可以書寫PHP變量,在變量解析那里可以看到這個問題),緊接著一個while循環掃描到*/的位置,如果一直到文件結尾都沒掃到*/,那就zend_error一個Waring錯誤,但是不會影響接下去的解析。

PHP數字類型

從一開始的正則規則里邊可以知道PHP支持5中類型的數字常量聲明:其實對于代碼來說,數字其實也是字符,詞法分析器掃描到這5個規則的時候,需要把當前的zendlval對應的解析成數字存起來,同時返回一個數字類型的Token標志,看最簡單的LNUM規則處理:首先檢查一下當前的字符串是否超出C語言的long類型長度,如果不超過,直接接調用strtol把字符串轉換成long int類型。如果超出了long的范圍,Zend還是嘗試看看能不能轉,如果發生溢出(error == ERANGE)那就把當前數字轉成double類型。至于DNUM、BNUM等就不占篇幅了。

PHP變量類型

PHP的變量是以美元符$開頭,從詞法規則里邊可以看到:有三種變量的聲明調用方式,$var, $var->PRop, $var["key"]。注意到yyless調用,yyless的宏定義聲明在69行:因為詞法掃描的時候已經吃掉了"$var->",而我們只需要提取出變量名"var",因此我們需要讓YYCURSOR指針重新回到"var->"的"-"位置,因此調用了yyless(yyleng-3)。緊接著都是通過zend_copy_value拷貝變量名到zendlval里邊記錄起來供之后語法解析階段插入到符號表里邊去。這里再討論一個關于$var->prop的規則,我們留意到1193行有個奇怪的規則,為什么在ST_LOOKING_FOR_PROPERTY下還可以再有->呢,研究了一下,原來這里是為了檢驗$var->prop1->prop2這第2+個的->。

PHP字符串類型

PHP的字符串類型在詞法分析階段應該是最復雜的,PHP里邊的字符串可以由單引號跟雙引號來圍住,單引號的字符串比雙引號的字符串效率會更高,一會我們可以看到為什么。先來看一下單引號的規則:首先留意到b?['],字符串前邊能加上b聲明?但是在之后的代碼中壓根沒看出這個b的聲明對字符串有什么影響。在http://php.net/manual/zh/language.types.string.php里邊有這樣一句描述:原來這b是為了聲明一個二進制字符串用的。再留意到2022行,為什么遇到'//'要讓YYCURSOR++呢?因為在字符串中/后邊帶的是轉義字符,這里讓YYCURSOR++的目的就是為了跳過下一個字符,例如:'/'',如果不跳過第二個單引號的話,我們掃描到第二個引號就會認為字符串結束了。接下去的處理就比較簡單了,從輸入流中取出字符串的內容,返回一個T_CONSTANT_ENCAPSED_STRING的Token標志。雙引號的字符串處理就復雜一點了:雙引號里邊是支持變量的!$hello = "Hello"; $str = "${hello} World";留意到2085行,如果雙引號字符串里邊沒有變量,直接就返回一個字符串了,從這里看出,其實雙引號字符串在沒有包含$的情況下的效率跟單引號字符串是差不多的。如果遇到了變量!這個時候就要切換到ST_DOUBLE_QUOTES狀態了:現在又回到了尋找變量的規則,其他的規則就不占篇幅了,討論一個細節,我們回到1871行:注意到掃描到"$var["這種情況的時候,會壓入一個新的狀態ST_VAR_OFFSET,同時在1889這條規則里邊有前置條件ST_VAR_OFFSET的存在,這個是為了掃描到$var[$key][$key]這樣的情況,細心點還可以留意到字符串里邊的數組變量的key是不允許用->的,例如:$str = "$var[$a->s]";這樣是不符合語法的,會出現一個解析錯誤:Parse error: syntax error, unexpected '-', expecting ']' in xxx.php

PHP魔術變量

PHP魔術變量分為編譯時替換以及運行時替換,詞法規則文件里邊的1593-1722行定義了以下魔術變量:__CLASS__, __TRAIT__, __FUNCTION__, __METHOD__, __LINE__, __FILE__, __DIR__, __NAMESPACE__魔術變量的剖析留到之后再寫,留意到__contruct這類并不在詞法聲明的規則里邊出現。

PHP的容錯機制

在前邊說單行注釋的時候已經描述了一種容錯機制,在語法文件的1490行,2432行均有詞法分析階段的容錯機制。

結語

文章中還忽略了單字符的詞素(規則位于1454行)以及強制類型轉換的規則(例如:(int)$str, 規則位于1230行),Zend引擎的在詞法分析階段開始前還會檢查文件的編碼問題以及文件流的操作問題,之后再找篇文章細細研究一下這兩塊的內容。最后不由得不感嘆一下,盡管對編譯原理的熟悉程度不高,但是re2c的書寫出來的規則真心容易懂。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 漳州市| 合肥市| 新邵县| 义马市| 华容县| 横山县| 沙湾县| 阜城县| 海伦市| 巩义市| 武陟县| 湖南省| 南宫市| 集贤县| 虹口区| 望都县| 万州区| 九江县| 新田县| 阳原县| 韶关市| 乌审旗| 来宾市| 蒙自县| 嵊泗县| 贵定县| 乐东| 广州市| 麻栗坡县| 闻喜县| 茂名市| 扶沟县| 天全县| 纳雍县| 屏东市| 富民县| 东乌| 肃北| 南汇区| 肇州县| 东平县|