摘要:中詞法語法分析,生成抽象語法樹,然后編譯成及被執(zhí)行均由虛擬機(jī)完成。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。指令虛擬機(jī)的指令稱為,每條指令對應(yīng)一個。
作者 陳雷
編程語言的虛擬機(jī)是一種可以運(yùn)行中間語言的程序。中間語言是抽象出的指令集,由原生語言編譯而成,作為虛擬機(jī)執(zhí)行階段的輸入。很多語言都實(shí)現(xiàn)了自己的虛擬機(jī),比如Java、C#和Lua。PHP語言也有自己的虛擬機(jī),稱為Zend虛擬機(jī)。
PHP7完成基本的準(zhǔn)備工作后,會啟動Zend引擎,加載注冊的擴(kuò)展模塊,然后讀取對應(yīng)的腳本文件,Zend引擎會對文件進(jìn)行詞法和語法分析,生成抽象語法樹,接著抽象語法樹被編譯成Opcodes,如果開啟了Opcache,編譯的環(huán)節(jié)會被跳過從Opcache中直接讀取Opcodes進(jìn)行執(zhí)行。
PHP7中詞法語法分析,生成抽象語法樹,然后編譯成Opcodes及被執(zhí)行均由Zend虛擬機(jī)完成。這里將詳細(xì)闡述抽象語法樹編譯成Opcodes的過程,以及Opcodes被執(zhí)行的過程,來闡述Zend虛擬機(jī)的實(shí)現(xiàn)原理及關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)。
1 基礎(chǔ)知識Zend虛擬機(jī)(稱為Zend VM)是PHP語言的核心,承擔(dān)了語法詞法解析、抽象語法樹編譯以及指令的執(zhí)行工作,下面我們討論一下Zend虛擬機(jī)的基礎(chǔ)架構(gòu)以及相關(guān)的基礎(chǔ)知識。
1.1 Zend虛擬機(jī)架構(gòu)Zend虛擬機(jī)主要分為解釋層、中間數(shù)據(jù)層和執(zhí)行層,下面給出各層包含的內(nèi)容,如圖1所示。
圖1 Zend虛擬機(jī)架構(gòu)圖
下面解釋下各層的作用。
(1)解釋層
這一層主要負(fù)責(zé)把PHP代碼進(jìn)行詞法和語法分析,生成對應(yīng)的抽象語法樹;另一個工作就是把抽象語法樹進(jìn)行編譯,生成符號表和指令集;
(2)中間數(shù)據(jù)層
這一層主要包含了虛擬機(jī)的核心部分,執(zhí)行棧的維護(hù),指令集和符號表的存儲,而這三個是執(zhí)行引擎調(diào)度執(zhí)行的基礎(chǔ);
(3)執(zhí)行層
這一層是執(zhí)行指令集的引擎,這一層是最終的執(zhí)行并生成結(jié)果,這一層里面實(shí)現(xiàn)了大量的底層函數(shù)。
為了更好地理解Zend虛擬機(jī)各層的工作,我們先了解一下物理機(jī)的一些基礎(chǔ)知識,讀者可以對照理解虛擬機(jī)的原理。
1.2 符號表符號表是在編譯過程中,編譯程序用來記錄源程序中各種名字的特性信息,所以也稱為名字特性表。名字一般包含程序名、過程名、函數(shù)名、用戶定義類型名、變量名、常量名、枚舉值名、標(biāo)號名等。特性信息指的是名字的種類、類型、維數(shù)、參數(shù)個數(shù)、數(shù)值及目標(biāo)地址(存儲單元地址)等。
符號表有什么作用呢?一是協(xié)助進(jìn)行語義檢查,比如檢查一個名字的引用和之前的聲明是否相符,二是協(xié)助中間代碼生成,最重要的是在目標(biāo)代碼生成階段,當(dāng)需要為名字分配地址時,符號表中的信息將是地址分配的主要依據(jù)。
圖2 符號表創(chuàng)建示例
符號表一般有三種構(gòu)造和處理方法,分別是線性查找,二叉樹和Hash技術(shù),其中線性查找法是最簡單的,按照符號出現(xiàn)的順序填表,每次查找從第一個開始順序查找,效率比較低;二叉樹實(shí)現(xiàn)了對折查找,在一定程度上提高了效率;效率最高的是通過Hash技術(shù)實(shí)現(xiàn)符號表,相信大家對Hash技術(shù)有一定的了解,而PHP7中符號表就是使用的HashTable實(shí)現(xiàn)的。
1.3 函數(shù)調(diào)用棧為了更清晰地了解虛擬機(jī)中函數(shù)調(diào)用的過程,我們先了解一下物理機(jī)的簡單原理,主要涉及函數(shù)調(diào)用棧的概念,而Zend虛擬機(jī)參照了物理機(jī)的基本原理,做了類似的設(shè)計(jì)。
下面以一段C代碼描述一下系統(tǒng)棧和函數(shù)過程調(diào)用,代碼如下:
int funcB(int argB1, int argB2) { int varB1, varB2; return argB1+argB2; } int funcA(int argA1, int argA2) { int varA1, varA2; return argA1+argA2+funcB( 3, 4); } int main() { int varMain; return funcA(1, 2); }
這段代碼運(yùn)行時,首先main函數(shù)會壓棧, 首先將局部變量varMain入棧,main函數(shù)調(diào)用了funcA函數(shù),C語言會從后往前,將函數(shù)參數(shù)壓棧,先壓第二個參數(shù)argA2=2,再壓第一個參數(shù)argA1=1,同時對于funcA的返回會產(chǎn)生一個臨時變量等待賦值,也會被壓棧,這些稱為main函數(shù)的棧幀;接著將funcA壓棧,同樣的先將局部變量varA1和varA2壓入棧中,因?yàn)檎{(diào)用了函數(shù)funcB,會將參數(shù)argB2=4和argB1=3壓入棧中,同時把funcB的返回產(chǎn)生的臨時變量壓入棧中,這部分稱為funcA的棧幀;同樣,funcB被壓入棧中,如圖3所示。
圖3 函數(shù)調(diào)用壓棧過程示意圖
funcB函數(shù)執(zhí)行,對argB1和argB2進(jìn)行相加操作,執(zhí)行后得到返回值為7,然后funcB的棧幀出棧,funcA中臨時變量TempB被賦值為7,繼而進(jìn)行相加操作,得到結(jié)果為10,然后funcA出棧,main函數(shù)中臨時變量TempA被賦值為10,最終main函數(shù)返回并出棧,整個函數(shù)調(diào)用結(jié)束。如圖4所示。
圖4 函數(shù)調(diào)用出棧過程示意圖
匯編語句中的指令語句一般格式為:
[標(biāo)號:] [前綴] 指令助記符 [操作數(shù)] [;注釋]
其中:
1)標(biāo)識符字段由各種有效字符組成,一般表示符號地址,具有段基址、偏移量、類型三種屬性。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。
2)助記符,規(guī)定指令或偽指令的操作功能,是語句中唯一不可缺少的部分。對于指令,匯編程序會將其翻譯成機(jī)器語言指令:
MOV AX, 100 → B8 00 01
3)操作數(shù),指令語句中提供給指令的操作對象、存放位置。操作數(shù)可以是1個、2個或0個,2個時用逗號‘,’分開。比如“RET;”對應(yīng)的操作數(shù)個數(shù)是0個,“INC BX;”對應(yīng)的操作數(shù)個數(shù)是1,“MOV AX,DATA;”對應(yīng)的操作數(shù)個數(shù)是2個。
4)注釋,以“ ;”開始,給以編程說明。
符號表、函數(shù)調(diào)用棧以及指令基本構(gòu)成了物理機(jī)執(zhí)行的基本元素,Zend虛擬機(jī)也同樣實(shí)現(xiàn)了符號表,函數(shù)調(diào)用棧及指令,來運(yùn)行PHP代碼,下面我先討論一下Zend虛擬機(jī)相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
2相關(guān)數(shù)據(jù)結(jié)構(gòu)Zend虛擬機(jī)包含了詞法語法分析,抽象語法樹的編譯,以及Opcodes的執(zhí)行,本文主要詳細(xì)介紹抽象語法樹和Opcodes的執(zhí)行過程,在展開介紹之前,先闡述一下用到的基本的數(shù)據(jù)結(jié)構(gòu),為后面原理性的介紹奠定基礎(chǔ)。
2.1 EG(v)首先介紹的是全局變量executor_globals,EG(v)是對應(yīng)的取值宏,executor_globals對應(yīng)的是結(jié)構(gòu)體_zend_executor_globals,是PHP生命周期中非常核心的數(shù)據(jù)結(jié)構(gòu)。這個結(jié)構(gòu)體中維護(hù)了符號表(symbol_table, function_table,class_table等),執(zhí)行棧(zend_vm_stack)以及包含執(zhí)行指令的zend_execute_data,另外還包含了include的文件列表,autoload函數(shù),異常處理handler等重要信息,下面給出_zend_executor_globals的結(jié)構(gòu)圖,然后分別闡述其含義,如圖5所示。
圖5 EG(v)結(jié)構(gòu)圖
這個結(jié)構(gòu)體比較復(fù)雜,下面我們介紹幾個核心的成員。
1)symbol_table:符號表,這里面主要是存的全局變量,以及一些魔術(shù)變量,比如$_GET、$_POST等;
2)function_table:函數(shù)表,主要存放函數(shù),包括大量的內(nèi)部函數(shù),以及用戶自定義的函數(shù),比如zend_version,func_num_args,str系列函數(shù),等等;
3)class_table:類表,主要存放內(nèi)置的類以及用戶自定義的類,比如stdclass、throwable、exception等類;
4)zend_constants:常量表,存放PHP中的常量,比如E_ERROR、E_WARNING等;
5)vm_stack:虛擬機(jī)的棧,執(zhí)行時壓棧出棧都在這上面操作;
6)current_execute_data:對應(yīng)_zend_execute_data結(jié)構(gòu)體,存放執(zhí)行時的數(shù)據(jù)。
下面針對于符號表、指令集、執(zhí)行數(shù)據(jù)和執(zhí)行棧進(jìn)行詳細(xì)介紹。
2.2 符號表PHP7中符號表分為了symbol_table、function_table和class_table等。
(1)symbol_table
symbol_table里面存放了變量信息,其類型是HashTable,下面我們看一下具體的定義:
//符號表緩存 zend_array *symtable_cache[SYMTABLE_CACHE_SIZE]; zend_array **symtable_cache_limit; zend_array **symtable_cache_ptr; //符號表 zend_array symbol_table;
symbol_table里面有什么呢,代碼”$a=1;”對應(yīng)的symnol_table,如圖6所示。
圖6 symbol_table示意圖
從圖6中可以看出,符號表中有我們常見的超全局變量$_GET、$_POST等,還有全局變量$a。在編譯過程中會調(diào)用zend_attach_symbol_table函數(shù)將變量加入symbol_table中。
(2)function_table
function_table對應(yīng)的是函數(shù)表,其類型也是HashTable,見代碼:
HashTable *function_table; /* function symbol table */
函數(shù)表中存儲哪些函數(shù)呢?同樣以上述代碼為例,我們利用GDB印一下function_table的內(nèi)容:
(gdb) p *executor_globals.function_table $1 = {gc = {refcount = 1, u = {v = {type = 7 "a", flags = 0 "