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