摘要:在中,源代碼首先將進(jìn)行詞法分析,將源代碼切割為多個(gè)字符串單元,分割后的字符串稱之為。圖以為例解釋型語(yǔ)言的執(zhí)行示意圖第步源碼通過(guò)詞法分析得到第步基于語(yǔ)法分析器生成抽象語(yǔ)法樹(shù)第步抽象語(yǔ)法樹(shù)轉(zhuǎn)換為指令集合,解釋執(zhí)行。
順風(fēng)車運(yùn)營(yíng)研發(fā)團(tuán)隊(duì) 李志 發(fā)表在程序人生 公眾號(hào)
我們常用的高級(jí)語(yǔ)言有很多種,比較出名的有CC++、Python、 PHP、Go、Pascal等。而這些語(yǔ)言根據(jù)運(yùn)行的方式不同,大體分為兩種:編譯型語(yǔ)言和解釋型語(yǔ)言。
其中,編譯型語(yǔ)言包括CC++、Pascal、Go等。這里說(shuō)的編譯是指在應(yīng)用源程序執(zhí)行之前,就將程序源代碼“翻譯”成匯編語(yǔ)言,然后進(jìn)一步根據(jù)軟硬件環(huán)境編譯成目標(biāo)文件。一般我們稱完成編譯工作的工具叫編譯器。而解釋型語(yǔ)言,在程序運(yùn)行時(shí)才被“翻譯”為機(jī)器語(yǔ)言。但是執(zhí)行一次“翻譯”一次,所以執(zhí)行效率較低。解釋器的工作就是解釋性語(yǔ)言中,負(fù)責(zé)“翻譯”源代碼的程序。
下面我們更詳細(xì)地討論一下編譯型語(yǔ)言和解釋性語(yǔ)言的運(yùn)行方式。
一、編譯型語(yǔ)言與解釋型語(yǔ)言
我們知道,對(duì)于一段C語(yǔ)言代碼,需要經(jīng)過(guò)預(yù)編譯、編譯、匯編和鏈接,才能成為可執(zhí)行的二進(jìn)制文件。以hello.c為例:
#includeint main(){ printf("hello world"); return 1; }
對(duì)于這段C代碼,main是程序入口函數(shù),實(shí)現(xiàn)的功能是打印字符串“hello world” 到屏幕上。編譯和執(zhí)行過(guò)程如圖1所示。
圖1 編譯型語(yǔ)言的執(zhí)行示意圖
第1步:C語(yǔ)言代碼預(yù)處理(比如依賴處理、宏替換等)。如以上代碼示例,#inlcude
第2步:編譯。編譯器會(huì)把C語(yǔ)言翻譯成匯編語(yǔ)言程序,一條C語(yǔ)言通常便以為多條匯編代碼。同時(shí)編譯器會(huì)對(duì)程序進(jìn)行優(yōu)化,生成目標(biāo)匯編程序。
第3步:編譯得到的匯編語(yǔ)言通過(guò)匯編器再匯編成目標(biāo)程序hello.o。
第4步:鏈接。程序中往往包含一些共享目標(biāo)文件,如示例程序中的printf()函數(shù),位于靜態(tài)庫(kù),需要經(jīng)過(guò)鏈接器(如Uinx連接器ld)進(jìn)行鏈接。
以C語(yǔ)言為代表的編譯型語(yǔ)言,代碼發(fā)生更新都要經(jīng)過(guò)以上步驟:
我們區(qū)別編譯型語(yǔ)言與解釋型語(yǔ)言,主要立足于源代碼被編譯成目標(biāo)平臺(tái)CPU指令的時(shí)機(jī)。對(duì)于編譯型語(yǔ)言,編譯結(jié)果已經(jīng)是針對(duì)當(dāng)前CPU體系的指令;而解釋型語(yǔ)言,需要先編譯成中間代碼,再經(jīng)由該解釋型語(yǔ)言的特定虛擬機(jī),翻譯成特定CPU體系的指令被執(zhí)行。解釋型語(yǔ)言是在運(yùn)行過(guò)程中,翻譯為目標(biāo)平臺(tái)的指令。常說(shuō)解釋型語(yǔ)言“慢”,主要也是慢在這里。
在PHP7中,源代碼首先將進(jìn)行詞法分析,將源代碼切割為多個(gè)字符串單元,分割后的字符串稱之為Token。而一個(gè)個(gè)獨(dú)立的Token無(wú)法表達(dá)完整語(yǔ)義,需經(jīng)過(guò)語(yǔ)法分析階段,將Token轉(zhuǎn)換為抽象語(yǔ)法樹(shù)(簡(jiǎn)稱AST)。之后,抽象語(yǔ)法樹(shù)被轉(zhuǎn)換為機(jī)器指令執(zhí)行。在PHP中,這些指令稱為opcode(后文會(huì)對(duì)opcode做更詳細(xì)的解釋,此處讀者可以看待為CPU指令)。
到AST的生成這一步,編譯型語(yǔ)言與解釋型語(yǔ)言所需經(jīng)歷的過(guò)程相似。從抽象語(yǔ)法樹(shù)之后開(kāi)始產(chǎn)生差異。
圖2是PHP(如無(wú)特殊說(shuō)明,本章提到的PHP均為PHP7版本)代碼被執(zhí)行的簡(jiǎn)化步驟,其中最后一步的左側(cè)分支,是編譯型語(yǔ)言的過(guò)程。
圖2 以PHP為例解釋型語(yǔ)言的執(zhí)行示意圖
第1步:源碼通過(guò)詞法分析得到Token;
第2步:基于語(yǔ)法分析器生成抽象語(yǔ)法樹(shù)(AST);
第3步:抽象語(yǔ)法樹(shù)轉(zhuǎn)換為Opcodes(opcode指令集合),PHP解釋執(zhí)行Opcodes。
接下來(lái)我們?cè)诨静襟E的基礎(chǔ)上,細(xì)化PHP語(yǔ)言的執(zhí)行原理,試圖更清晰地建立認(rèn)知。
二、PHP7的執(zhí)行原理概述
首先我們補(bǔ)充說(shuō)明下前文提到的PHP7程序執(zhí)行過(guò)程,請(qǐng)參見(jiàn)圖3。
圖3 PHP7語(yǔ)言編寫的程序的執(zhí)行過(guò)程圖
第1步:詞法分析將PHP代碼轉(zhuǎn)換為有意義的標(biāo)識(shí)Token。該步驟的詞法分析器使用Re2c實(shí)現(xiàn)的。
第2步:語(yǔ)法分析將Token和符合文法規(guī)則的代碼生成抽象語(yǔ)法樹(shù)。語(yǔ)法分析器基于Bison實(shí)現(xiàn)。語(yǔ)法分析使用了巴科斯范式(BNF)來(lái)表達(dá)文法規(guī)則,Bison借助狀態(tài)機(jī)、狀態(tài)轉(zhuǎn)移表和壓棧、出棧等一系列操作,生成抽象語(yǔ)法樹(shù)。
第3步:上步的抽象語(yǔ)法樹(shù)生成對(duì)應(yīng)的opcode,被虛擬機(jī)執(zhí)行。opcode是PHP7定義的一組指令標(biāo)識(shí),指令對(duì)應(yīng)著相應(yīng)的handler(處理函數(shù))。當(dāng)虛擬機(jī)調(diào)用opcode,會(huì)找到opcode背后的處理函數(shù),執(zhí)行真正的處理。以我們常見(jiàn)的echo語(yǔ)句為例,其對(duì)應(yīng)的opcode便是ZEND_ECHO。
注意:這里為了便于理解詞法分析和語(yǔ)法分析過(guò)程,將兩者分開(kāi)描述。但實(shí)際情況,出于效率考慮,兩個(gè)過(guò)程并非完全獨(dú)立。
下面,我們通過(guò)一段示例代碼,來(lái)建立PHP7運(yùn)轉(zhuǎn)的初步理解。
示例代碼如下:
從圖3可知,這段代碼首先會(huì)被切割為Token。
1. Token
Token是PHP代碼被切割成的有意義的標(biāo)識(shí)。本書介紹的PHP7版本中有137 種Token,在zend_language_parser.h文件中做了定義:
/* Tokens. */ #define END 0 #define T_INCLUDE 258 #define T_INCLUDE_ONCE 259 … #define T_ERROR 392更多Token的含義,感興趣的讀者可以參考《PHP 7底層設(shè)計(jì)與源碼實(shí)現(xiàn)》附錄。
PHP提供了token_get_all()函數(shù)來(lái)獲取PHP代碼被切割后的Token,可以在深入源碼學(xué)習(xí)前,粗略查看PHP代碼被切割后的Token。如下代碼片段:
/home/vagrant/php7/bin/php –r "print_r(Token_get_all("輸出結(jié)果為:
Array ( [0] => Array ( [0] => 379 [1] => 1 ) [1] => Array ( [0] => 328 [1] => echo [2] => 1 ) [2] => Array ( [0] => 382 [1] => [2] => 1 ) [3] => Array ( [0] => 323 [1] => "hello world" [2] => 1 ) [4] => ; )上文輸出中,二維數(shù)組的每個(gè)成員數(shù)組第一個(gè)值為Token對(duì)應(yīng)的枚舉值;第二個(gè)值為Token對(duì)應(yīng)的原始字符串內(nèi)容;第三個(gè)值為代碼對(duì)應(yīng)的行號(hào)??梢钥闯觯~法解析器將
1)文本“
#dfine T_OPEN_TAG 379不難理解,它是PHP代碼的起始tag,也就是
2)echo對(duì)應(yīng)的Token是T_ECHO:
#define T_ECHO 3283)源碼中的空格,對(duì)應(yīng)的Token叫T_WHITESPACE,值為382:
#define T_WHITESPACE 3824)字符串“hello world”對(duì)應(yīng)的Token值為323:
#define T_CONSTANT_ENCAPSED_STRING 323可見(jiàn),Token就是一個(gè)個(gè)的“詞塊”,但是多帶帶存在的詞塊不能表達(dá)完整的語(yǔ)義,還需要借助規(guī)則進(jìn)行組織串聯(lián)。語(yǔ)法分析器就是這個(gè)組織者。它會(huì)檢查語(yǔ)法、匹配Token,對(duì)Token進(jìn)行關(guān)聯(lián)。
PHP7中,組織串聯(lián)的產(chǎn)物就是抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST)。
2. AST
AST是PHP7版本新特性。在這之前的版本,PHP代碼的執(zhí)行過(guò)程中沒(méi)有生成AST這一步。PHP7對(duì)抽象語(yǔ)法樹(shù)的支持,實(shí)現(xiàn)了PHP編譯器和解釋器解耦,有效提升了可維護(hù)性。
顧名思義,抽象語(yǔ)法樹(shù)具有樹(shù)狀結(jié)構(gòu)。AST的節(jié)點(diǎn)分為多種類型,對(duì)應(yīng)著不同的PHP語(yǔ)法。在當(dāng)前章節(jié),我們可以認(rèn)為節(jié)點(diǎn)類型是對(duì)語(yǔ)法規(guī)則的抽象,例如賦值語(yǔ)句,生成的抽象語(yǔ)法樹(shù)節(jié)點(diǎn)為ZEND_AST_ASSIGN。而賦值語(yǔ)句的左右操作數(shù),又將作為ZEND_AST_ASSIGN類型節(jié)點(diǎn)的孩子。通過(guò)這樣的節(jié)點(diǎn)關(guān)系,構(gòu)建出抽象語(yǔ)法樹(shù)。
如果讀者希望一睹為快,可以直接跳到本書第13章函數(shù)的實(shí)現(xiàn),其中圖片描繪了一段簡(jiǎn)單的PHP代碼生成的抽象語(yǔ)法樹(shù)。
在這里,我們推薦讀者了解下PhpParser工具,可以用它來(lái)查看PHP代碼生成的AST。
注意:PHP-Parser是PHP7內(nèi)核作者之一nikic編寫的將PHP源碼生成AST的工具。源碼見(jiàn)https://github.com/nikic/PHP-...
3. Opcodes
AST扮演了源碼到中間代碼的臨時(shí)存儲(chǔ)介質(zhì)的角色,還需要將其轉(zhuǎn)換為opcode,才能被引擎直接執(zhí)行。Opcode只是單條指令,Opcodes是opcode的集合形式,是PHP執(zhí)行過(guò)程中的中間代碼,類似Java中的字節(jié)碼。生成之后由虛擬機(jī)執(zhí)行。
我們知道,PHP工程優(yōu)化措施中有個(gè)比較常見(jiàn)的“開(kāi)啟Opcache”,指的就是這里的Opcodes的緩存(Opcodes Cache)。通過(guò)省去從源碼到opcode的階段,引擎可以直接執(zhí)行緩存的opcode,以此提升性能。
借助vld插件,可以直觀地看到一段PHP代碼生成的opcode:
php -dvld.active=1 hello.php 經(jīng)過(guò)過(guò)濾整理,對(duì)應(yīng)的opcode為: line op 1 ECHO 2 RETURN其實(shí)在源碼實(shí)現(xiàn)中,上述代碼生成的opcode及handler為:
ZEND_ECHO // handler: ZEND_ECHO_SPEC_CONST_HANDLER ZEND_RETURN // handler: ZEND_RETURN_SPEC_CONST_HANDLER可見(jiàn),ZEND_ECHO對(duì)應(yīng)的handler是ZEND_ECHO_SPEC_CONST_HANDLER。此handler的實(shí)現(xiàn)的功能便是預(yù)期的“hello world”語(yǔ)句的輸出。
本書的PHP版本中,內(nèi)核在zend_vm_opcodes.h中定義了186種Opcodes,也可以參考《PHP 7底層設(shè)計(jì)與源碼實(shí)現(xiàn)》附錄部分。
在平時(shí)的業(yè)務(wù)開(kāi)發(fā)中,了解一些 PHP的底層實(shí)現(xiàn),尤其是語(yǔ)法機(jī)制的實(shí)現(xiàn),對(duì)性能提升、故障排除非常有好處。希望這篇淺文,可以幫助讀者在使用PHP7的同時(shí),了解到編寫的PHP代碼如何被編譯和執(zhí)行。
(掃上方二維碼7.9折購(gòu)買)
推薦理由:
滴滴出行專家聯(lián)合撰寫,PHP領(lǐng)域大咖夏緒宏、韓天峰、王晶、謝華亮(黑夜路人)、伍星聯(lián)袂推薦
全面吃透PHP內(nèi)核架構(gòu)、核心實(shí)現(xiàn)與內(nèi)存管理、詞法與句法解析、Zend 虛擬機(jī)、函數(shù)及關(guān)鍵擴(kuò)展等設(shè)計(jì)細(xì)節(jié)與源碼實(shí)現(xiàn)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/28985.html
摘要:我們修改上面代碼,再來(lái)看下返回值類型限制的情況運(yùn)行結(jié)果這段代碼我們額外聲明了返回值的類型為型。對(duì)函數(shù)返回值的聲明做了擴(kuò)充,可以定義其返回值為,無(wú)論是否開(kāi)啟嚴(yán)格模式,只要函數(shù)中有以外的其他語(yǔ)句都會(huì)報(bào)錯(cuò)。 順風(fēng)車運(yùn)營(yíng)研發(fā)團(tuán)隊(duì) 王坤 發(fā)表至21CTO公眾號(hào)(https://mp.weixin.qq.com/s/ph...) showImg(https://segmentfault.c...
摘要:中詞法語(yǔ)法分析,生成抽象語(yǔ)法樹(shù),然后編譯成及被執(zhí)行均由虛擬機(jī)完成。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。指令虛擬機(jī)的指令稱為,每條指令對(duì)應(yīng)一個(gè)。 作者 陳雷編程語(yǔ)言的虛擬機(jī)是一種可以運(yùn)行中間語(yǔ)言的程序。中間語(yǔ)言是抽象出的指令集,由原生語(yǔ)言編譯而成,作為虛擬機(jī)執(zhí)行階段的輸入。很多語(yǔ)言都實(shí)現(xiàn)了自己的虛擬機(jī),比如Java、C#和Lua。PHP語(yǔ)言也有自己的虛擬機(jī),稱為Z...
摘要:重點(diǎn)分析其中宏,返回表的,后面用來(lái)作為索引查詢的依據(jù)。中資源的解析比中解析簡(jiǎn)單快捷很多,得益于其結(jié)構(gòu)的改變。先從逆向通過(guò)其在中索引層層關(guān)聯(lián),找到該類資源的釋放回調(diào)函數(shù),然后對(duì)該資源執(zhí)行釋放回調(diào)函數(shù)。 PHP 擴(kuò)展開(kāi)發(fā)的文章,我均已更新至《TIPI》本篇承接上篇 PHP7 使用資源包裹第三方擴(kuò)展的實(shí)現(xiàn) PHP7 使用資源包裹第三方擴(kuò)展原理分析 注冊(cè)資源類型源碼 [c] ZEND_API ...
閱讀 836·2019-08-29 12:49
閱讀 3614·2019-08-29 11:32
閱讀 3535·2019-08-26 10:43
閱讀 2458·2019-08-23 16:53
閱讀 2113·2019-08-23 15:56
閱讀 1756·2019-08-23 12:03
閱讀 2827·2019-08-23 11:25
閱讀 2145·2019-08-22 15:11