做者 陳雷
編程語言的虛擬機是一種能夠運行中間語言的程序。中間語言是抽象出的指令集,由原生語言編譯而成,做爲虛擬機執行階段的輸入。不少語言都實現了本身的虛擬機,好比Java、C#和Lua。PHP語言也有本身的虛擬機,稱爲Zend虛擬機。php
PHP7完成基本的準備工做後,會啓動Zend引擎,加載註冊的擴展模塊,而後讀取對應的腳本文件,Zend引擎會對文件進行詞法和語法分析,生成抽象語法樹,接着抽象語法樹被編譯成Opcodes,若是開啓了Opcache,編譯的環節會被跳過從Opcache中直接讀取Opcodes進行執行。node
PHP7中詞法語法分析,生成抽象語法樹,而後編譯成Opcodes及被執行均由Zend虛擬機完成。這裏將詳細闡述抽象語法樹編譯成Opcodes的過程,以及Opcodes被執行的過程,來闡述Zend虛擬機的實現原理及關鍵的數據結構。編程
Zend虛擬機(稱爲Zend VM)是PHP語言的核心,承擔了語法詞法解析、抽象語法樹編譯以及指令的執行工做,下面咱們討論一下Zend虛擬機的基礎架構以及相關的基礎知識。數組
Zend虛擬機主要分爲解釋層、中間數據層和執行層,下面給出各層包含的內容,如圖1所示。緩存
圖1 Zend虛擬機架構圖數據結構
下面解釋下各層的做用。架構
(1)解釋層編程語言
這一層主要負責把PHP代碼進行詞法和語法分析,生成對應的抽象語法樹;另外一個工做就是把抽象語法樹進行編譯,生成符號表和指令集;函數
(2)中間數據層ui
這一層主要包含了虛擬機的核心部分,執行棧的維護,指令集和符號表的存儲,而這三個是執行引擎調度執行的基礎;
(3)執行層
這一層是執行指令集的引擎,這一層是最終的執行並生成結果,這一層裏面實現了大量的底層函數。
爲了更好地理解Zend虛擬機各層的工做,咱們先了解一下物理機的一些基礎知識,讀者能夠對照理解虛擬機的原理。
符號表是在編譯過程當中,編譯程序用來記錄源程序中各類名字的特性信息,因此也稱爲名字特性表。名字通常包含程序名、過程名、函數名、用戶定義類型名、變量名、常量名、枚舉值名、標號名等。特性信息指的是名字的種類、類型、維數、參數個數、數值及目標地址(存儲單元地址)等。
符號表有什麼做用呢?一是協助進行語義檢查,好比檢查一個名字的引用和以前的聲明是否相符,二是協助中間代碼生成,最重要的是在目標代碼生成階段,當須要爲名字分配地址時,符號表中的信息將是地址分配的主要依據。
圖2 符號表建立示例
符號表通常有三種構造和處理方法,分別是線性查找,二叉樹和Hash技術,其中線性查找法是最簡單的,按照符號出現的順序填表,每次查找從第一個開始順序查找,效率比較低;二叉樹實現了對摺查找,在必定程度上提升了效率;效率最高的是經過Hash技術實現符號表,相信你們對Hash技術有必定的瞭解,而PHP7中符號表就是使用的HashTable實現的。
爲了更清晰地瞭解虛擬機中函數調用的過程,咱們先了解一下物理機的簡單原理,主要涉及函數調用棧的概念,而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 函數調用出棧過程示意圖
彙編語句中的指令語句通常格式爲:
[標號:] [前綴] 指令助記符 [操做數] [;註釋]
其中:
MOV AX, 100 → B8 00 01
符號表、函數調用棧以及指令基本構成了物理機執行的基本元素,Zend虛擬機也一樣實現了符號表,函數調用棧及指令,來運行PHP代碼,下面我先討論一下Zend虛擬機相關的數據結構。
Zend虛擬機包含了詞法語法分析,抽象語法樹的編譯,以及Opcodes的執行,本文主要詳細介紹抽象語法樹和Opcodes的執行過程,在展開介紹以前,先闡述一下用到的基本的數據結構,爲後面原理性的介紹奠基基礎。
首先介紹的是全局變量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)結構圖
這個結構體比較複雜,下面咱們介紹幾個核心的成員。
下面針對於符號表、指令集、執行數據和執行棧進行詳細介紹。
PHP7中符號表分爲了symbol_table、function_table和class_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中。
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 '\000', gc_info = 0}, type_info = 7}}, u = {v = { flags = 25 '\031', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', consistency = 0 '\000'}, flags = 25}, nTableMask = 4294966272, arData = 0x12102b0, nNumUsed = 848, nNumOfElements = 848, nTableSize = 1024, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x8d0dc3 <zend_function_dtor>}
能夠看出,函數表中有大量的函數,上面打印顯示有848個之多,這裏面主要是內部函數,好比zend_version、func_num_args、cli_get_process_title,等等。
class_table對應的是類表,其也是HashTable:
HashTable *class_table; /* class table */
類表裏面也有大量的內置類,好比stdclass、traversable、xmlreader等。
符號表裏面存放了執行時須要的數據,好比在symbol_table中,key爲_GET的Bucket對應的又是個HashTable,裏面存放的是$_GET[xxx],執行時會從中取對應的值。
Zend虛擬機的指令稱爲opline,每條指令對應一個opcode。PHP代碼在編譯後生成opline,Zend虛擬機根據不一樣的opline完成PHP代碼的執行,opline由操做指令、操做數和返回值組成,與機器指令很是相似,opline對應的結構體爲zend_op,見代碼:
struct _zend_op { const void *handler; //操做執行的函數 znode_op op1; //操做數1 znode_op op2; //操做數2 znode_op result; //返回值 uint32_t extended_value; //擴展值 uint32_t lineno; //行號 zend_uchar opcode; //opcode值 zend_uchar op1_type; //操做數1的類型 zend_uchar op2_type; //操做數2的類型 zend_uchar result_type; //返回值的類型 };
對應的內存佔用圖如圖7所示。
圖7 zend_op結構圖
PHP代碼會被編譯成一條一條的opline,分解爲最基本的操做,舉個例子,若是把opcode當成一個計算器,只接受兩個操做數op1和 op2,執行一個操做handler,好比加減乘除,而後它返回一個結果result,再稍加處理算術溢出的狀況存在extended_value中。下面詳細介紹下各個字段。
Opcode有時候被稱爲所謂的字節碼(Bytecode),是被軟件解釋器解釋執行的指令集。這些軟件指令集一般會提供一些比對應的硬件指令集更高級的數據類型和操做。
注意:Opcode和Bytecode實際上是兩個含義不一樣的詞,但常常會把它們看成同一個意思來交互使用。
Zend虛擬機有很是多Opcode,對應能夠作很是多事情,而且隨着PHP的發展, Opcode也愈來愈多,意味着PHP能夠作愈來愈多的事情。全部的Opcode都在PHP的源代碼文件Zend/zend_vm_opcodes.h中定義。Opcode的名稱是自描述的,好比:
PHP 7.1.0中有186個Opcode:
#define ZEND_NOP 0 #define ZEND_ADD 1 #define ZEND_SUB 2 #define ZEND_MUL 3 #define ZEND_DIV 4 #define ZEND_MOD 5 #define ZEND_SL 6 … #define ZEND_FETCH_THIS 184 #define ZEND_ISSET_ISEMPTY_THIS 186 #define ZEND_VM_LAST_OPCODE 186
op1和op2都是操做數,但不必定所有使用,也就是說每一個Opcode對應的hanlder最多可使用兩個操做數(也能夠只使用其中一個,或者都不使用)。每一個操做數均可以理解爲函數的參數,返回值result是hanlder函數對操做數op1和op2計算後的結果。op一、op2和result對應的類型都是znode_op,其定義爲一個聯合體:
typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; /* Needs to be signed */ #if ZEND_USE_ABS_JMP_ADDR zend_op *jmp_addr; #else uint32_t jmp_offset; #endif #if ZEND_USE_ABS_CONST_ADDR zval *zv; #endif } znode_op;
這樣其實每一個操做數都是uint32類型的數字,通常表示的是變量的位置。操做數有5種不一樣的類型,具體在Zend/zend_compile.h中定義:
#define IS_CONST (1<<0) #define IS_TMP_VAR (1<<1) #define IS_VAR (1<<2) #define IS_UNUSED (1<<3) /* Unused variable */ #define IS_CV (1<<4) /* Compiled variable */
這些類型是按位表示的,具體含義以下。
handler爲Opcode對應的是實際的處理函數,Zend虛擬機對每一個Opcode的工做方式是徹底相同的,都有一個handler的函數指針,指向處理函數的地址,這是一個C函數,包含了執行Opcode對應的代碼,使用op1,op2作爲參數,執行完成後,會返回一個結果result,有時也會附加一段信息extended_value。文件Zend/zend_vm_execute.h包含了全部的handler對應的函數,php-7.1.0中這個文件有62000+行。
注意:Zend/zend_vm_execute.h並不是手動編寫的,而是由zend_vm_gen.php這個PHP腳本解析zend_vm_def.h和zend_vm_execute.skl後生成,這個頗有意思,先有雞仍是先有蛋?沒有PHP 哪來的這個php腳本呢?這個是後期產物,早期php版本不用這個。這個相似於GO語言的自舉,本身編譯本身。
同一個Opcode對應的handler函數會根據操做數的類型而不一樣,好比ZEND_ASSIGN對應的handler就有多個:
ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_USED_HANDLER, ZEND_ASSIGN_SPEC_CV_CV_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_CV_CV_RETVAL_USED_HANDLER,
其函數命名是有以下規則的:
ZEND_[opcode]_SPEC_(操做數1類型)_(操做數2類型)_(返回值類型)_HANDLER
舉個例子,對於PHP代碼:
$a = 1;
對應的handler爲ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,其定義爲:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *value; zval *variable_ptr; SAVE_OPLINE(); //獲取op2對應的值,也就是1 value = EX_CONSTANT(opline->op2); //在execute_data中獲取op1的位置,也就是$a variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); /*代碼省略*/ //將1賦值給$a value = zend_assign_to_variable(variable_ptr, value, IS_CONST); } /*代碼省略*/ ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); }
從代碼中能夠很是直觀的看出,常量1是如何賦值給CV類型的$a的。
extended_value是存的擴展信息,Opcodes和CPU的指令相似,有一個標示指令字段handler,以及這個Opcode所操做的操做數op1和op2,但PHP不像彙編那麼底層, 在腳本實際執行的時候可能還須要其餘更多的信息,extended_value字段就保存了這類信息;
lineno對應源代碼文件中的行號。
到這裏,相信讀者對指令opline有了比較深入的認識,在Zend虛擬機執行時,這些指令被組裝在一塊兒,成爲指令集,下面咱們介紹一下指令集。
在介紹指令集前,須要先介紹一個編譯過程用到的一個基礎的結構體znode,其結構以下:
typedef struct _znode { /* used only during compilation */ zend_uchar op_type;//變量類型 zend_uchar flag; union { //表示變量的位置 znode_op op; //常量 zval constant; /* replaced by literal/zv */ } u; } znode
znode只會在編譯過程當中使用,其中op_type對應的是變量的類型,u是聯合體,u.op是操做數的類型,zval constant用來存常量。znode在後續生成指令集時會使用到。
Zend虛擬機中的指令集對應的結構爲zend_op_array,其結構以下:
struct _zend_op_array { /* Common elements */ /*代碼省略common是爲了函數可以快速訪問Opcodes而設定的*/ /* END of common elements */ //這部分是存放opline的數組,last爲總個數 uint32_t last; zend_op *opcodes; int last_var;//變量類型爲IS_CV的個數 uint32_t T;//變量類型爲IS_VAR和IS_TMP_VAR的個數 zend_string **vars;//存放IS_CV類型變量的數組 /*代碼省略*/ /* static variables support */ HashTable *static_variables; //靜態變量 /*代碼省略*/ int last_literal;//常量的個數 zval *literals;//常量數組 int cache_size;//運行時緩存數組大小 void **run_time_cache;//運行時緩存 void *reserved[ZEND_MAX_RESERVED_RESOURCES]; };
其結構圖如圖8所示。
圖8 zend_op_array結構圖
這個結構體中有幾個關鍵變量。
result->op_type = IS_CV; result->u.op.var = lookup_cv(CG(active_op_array), name); //lookup_cv: static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{ int i = 0; zend_ulong hash_value = zend_string_hash_val(name); //遍歷vars while (i < op_array->last_var) { //若是存在直接返回 if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) || (ZSTR_H(op_array->vars[i]) == hash_value && ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) && memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) { zend_string_release(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); } i++; } //不然插入到vars中,並將last_var的值設置爲該變量的操做數 i = op_array->last_var; op_array->last_var++; if (op_array->last_var > CG(context).vars_size) { CG(context).vars_size += 16; /* FIXME */ op_array->vars = erealloc(op_array->vars, CG(context).vars_size * sizeof(zend_string*)); } op_array->vars[i] = zend_new_interned_string(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); }
Zend在棧上執行的數據爲zend_execute_data,其結構體爲:
struct _zend_execute_data { const zend_op *opline; /* 要執行的指令 */ zend_execute_data *call; /* current call*/ zval *return_value; /* 返回值 */ zend_function *func; /* 執行函數 */ zval This; /* this + call_info + num_args */ zend_execute_data *prev_execute_data; zend_array *symbol_table; /*符號表*/ void **run_time_cache; /* 執行時緩存 */ zval *literals; /* 緩存常量 */ };
下面咱們介紹下各字段。
zend_execute_data是在執行棧上運行的關鍵數據,能夠用EX宏來取其中的值,見代碼:
#define EX(element) ((execute_data)->element)
瞭解完執行數據,下面接下來討論一下執行棧。
Zend虛擬機中有個相似函數調用棧的結構體,叫_zend_vm_stack。EG裏面的vm_stack也是這種類型的。其定義以下:
struct _zend_vm_stack { zval *top; //棧頂位置 zval *end; //棧底位置 zend_vm_stack prev; }; typedef struct _zend_vm_stack *zend_vm_stack;
能夠看出,棧的結構比較簡單,有三個變量top指向棧使用到的位置,end指向棧底,pre是指向上一個棧的指針,也就意味着全部棧在一個單向鏈表上。
PHP代碼在執行時,參數的壓棧操做,以及出棧調用執行函數都是在棧上進行的,下面介紹下棧操做的核心函數。
初始化調用的函數爲zend_vm_stack_init,主要進行內存申請,以及對_zend_vm_stack成員變量的初始化,見代碼:
ZEND_API void zend_vm_stack_init(void) { EG(vm_stack) = zend_vm_stack_new_page(ZEND_VM_STACK_PAGE_SIZE(0 /* main stack */), NULL); EG(vm_stack)->top++; EG(vm_stack_top) = EG(vm_stack)->top; EG(vm_stack_end) = EG(vm_stack)->end; }
該函數調首先調用zend_vm_stack_new_page爲EG(vm_stack)申請內存,申請的大小爲161024 sizeof(zval),見代碼:
static zend_always_inline zend_vm_stack zend_vm_stack_new_page(size_t size, zend_vm_stack prev) { zend_vm_stack page = (zend_vm_stack)emalloc(size); page->top = ZEND_VM_STACK_ELEMENTS(page); page->end = (zval*)((char*)page + size); page->prev = prev; return page; }
而後將zend_vm_stack的top指向zend_vm_stack的結束的位置,其中 zend_vm_stack佔用24個字節,end指向申請內存的最尾部,pre指向null,如圖9所示。
圖9 zend_vm_stack初始化後示意圖
能夠看出,多個vm_stack構成單鏈表,將多個棧鏈接起來,棧初始的大小爲16×1024個zval的大小,棧頂部佔用了一個*zval和struct _zend_vm_stack的大小,
調用的函數爲zend_vm_stack_push_call_frame,見代碼:
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object) { uint32_t used_stack = zend_vm_calc_used_stack(num_args, func); return zend_vm_stack_push_call_frame_ex(used_stack, call_info, func, num_args, called_scope, object); }
該函數會分配一塊用於當前做用域的內存空間,並返回zend_execute_data的起始位置。首先調用zend_vm_calc_used_stack計算棧須要的空間,見代碼:
static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func) { uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args; if (EXPECTED(ZEND_USER_CODE(func->type))) { used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args); } return used_stack * sizeof(zval); }
這段代碼是按照zval的大小對齊,咱們知道zval爲16字節,那麼對於zend_execute_data,大小爲80,那麼對應5個zval;同時對應IS_CV類型變量個數(last_var)以及變量類型爲IS_VAR和IS_TMP_VAR的個數(T),如圖10所示。
圖10 壓棧過程
到此,咱們瞭解了Zend虛擬機中符號表、指令集、執行數據以及執行棧相關的數據結構,下面咱們基於這些基本知識,來介紹一下指令集生成的過程。
抽象語法樹(AST)的編譯是生成指令集Opcodes的過程,詞法語法分析後生成的AST會保存在CG(ast)中,而後Zend虛擬機會將AST進一步轉換爲zend_op_array,以便在虛擬機中執行。下面咱們討論一下AST的編譯過程。
編譯過程在zend_compile函數中,在該函數裏,首先調用zendparse作了詞法和語法分析的工做,而後開始對CG(ast)的遍歷,根據節點不一樣的類型編譯爲不一樣指令opline,代碼以下:
static zend_op_array *zend_compile(int type) { /**代碼省略**/ if (!zendparse()) { //詞法語法分析 /**代碼省略**/ //初始化zend_op_array init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE); /**代碼省略**/ //遍歷ast生成opline zend_compile_top_stmt(CG(ast)); /**代碼省略**/ //設置handler pass_two(op_array); /**代碼省略**/ } /**代碼省略**/ return op_array; }
從上面的過程當中能夠看出,編譯的主要過程是op_array的初始化,調用zend_compile_top_stmt遍歷抽象語法樹成opline,以及調用pass_two函數設置handler。下面咱們一一闡述。
在遍歷抽象語法樹以前,須要先初始化指令集op_array,用來存放指令。op_array的初始化工做,調用的函數爲init_op_array,該函數會將op_array進行初始化,代碼以下:
op_array = emalloc(sizeof(zend_op_array)); init_op_array(op_array, type, INITIAL_OP_ARRAY_SIZE); void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size) { op_array->type = type; op_array->arg_flags[0] = 0; op_array->arg_flags[1] = 0; op_array->arg_flags[2] = 0; /**代碼省略**/ } CG(active_op_array) = op_array;
首先經過emalloc申請內存,大小爲sizeof(zend_op_array)=208,而後初始化op_array的全部成員變量,把op_array賦值給CG(active_op_array)。
抽象語法樹的編譯過程,是遍歷抽象語法樹,生成對應指令集的過程,編譯是在 zend_compile_top_stmt() 中完成,這個函數是總入口,會被屢次遞歸調用。其中傳入的參數爲CG(ast),這個AST是經過詞法和語法分析獲得的。下面咱們看一下zend_compile_top_stmt的代碼:
void zend_compile_top_stmt(zend_ast *ast) /* {{{ */ { if (!ast) { return; } //對於kind爲ZEND_AST_STMT_LIST的節點,轉換爲zend_ast_list if (ast->kind == ZEND_AST_STMT_LIST) { zend_ast_list *list = zend_ast_get_list(ast); uint32_t i; //根據children的個數進行遞歸調用 for (i = 0; i < list->children; ++i) { zend_compile_top_stmt(list->child[i]); } return; } //其餘kind的節點調用zend_compile_stmt zend_compile_stmt(ast); if (ast->kind != ZEND_AST_NAMESPACE && ast->kind != ZEND_AST_HALT_COMPILER) { zend_verify_namespace(); } if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) { CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno; zend_do_early_binding(); } }
從代碼中能夠看到,對於zend_compile_top_stmt,會對AST節點的kind進行判斷,而後走不一樣的邏輯,其實是對AST的深度遍歷,咱們如下面的代碼爲例,看一下對AST的遍歷過程。
<?php $a = 1; $b = $a + 2; echo $b;
能夠獲得的AST如圖11所示。
圖11 抽象語法樹示意圖
經過這課抽象語法樹。能夠很直觀的看出,CG(ast)節點下面有三個子女:
下面咱們看整棵樹的遍歷過程。
void zend_compile_stmt(zend_ast *ast) /* {{{ */ { /*…代碼省略…*/ switch (ast->kind) { /*…代碼省略…*/ default: { znode result; zend_compile_expr(&result, ast); zend_do_free(&result); } /*…代碼省略…*/ } void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */ { /*…代碼省略…*/ switch (ast->kind) { /*…代碼省略…*/ case ZEND_AST_ASSIGN: zend_compile_assign(result, ast); return; /*…代碼省略…*/ }
最終調用的函數爲zend_compile_assign,對ZEND_AST_ASSIGN節點進行編譯:
void zend_compile_assign(znode *result, zend_ast *ast) /* {{{ */ { zend_ast *var_ast = ast->child[0]; zend_ast *expr_ast = ast->child[1]; znode var_node, expr_node; zend_op *opline; uint32_t offset; /*…代碼省略…*/ switch (var_ast->kind) { case ZEND_AST_VAR: case ZEND_AST_STATIC_PROP: offset = zend_delayed_compile_begin(); zend_delayed_compile_var(&var_node, var_ast, BP_VAR_W); zend_compile_expr(&expr_node, expr_ast); zend_delayed_compile_end(offset); zend_emit_op(result, ZEND_ASSIGN, &var_node, &expr_node); return; /*…代碼省略…*/ }
從代碼中能夠看出,kind爲ZEND_AST_ASSIGN的抽象語法樹有兩個子女,左child爲var_ast,右child爲expr_ast,分別進行處理。
static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */ { return zend_stack_count (&CG(delayed_oplines_stack)); }
該函數會獲取CG的delayed_oplines_stack棧頂的位置,其中delayed_oplines_stack是對於依賴後續編譯動做存儲信息的棧。等expr_ast編譯後使用,調用zend_delayed_compile_end(offset)來獲取棧裏的信息。
void zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t type) /* {{{ */ { zend_op *opline; switch (ast->kind) { case ZEND_AST_VAR: zend_compile_simple_var(result, ast, type, 1); /**代碼省略**/ }
其中kind爲ZEND_AST_VAR,繼而調用zend_compile_simple_var函數:
static void zend_compile_simple_var(znode *result, zend_ast *ast, uint32_t type, int delayed) /* {{{ */ { zend_op *opline; /*…代碼省略…*/ } else if (zend_try_compile_cv(result, ast) == FAILURE) { /*…代碼省略…*/ } }
繼而調用zend_try_compile_cv函數:
static int zend_try_compile_cv(znode *result, zend_ast *ast) /* {{{ */ { zend_ast *name_ast = ast->child[0]; if (name_ast->kind == ZEND_AST_ZVAL) { /*…代碼省略…*/ result->op_type = IS_CV; result->u.op.var = lookup_cv(CG(active_op_array), name); /*…代碼省略…*/ }
核心函數是lookup_cv,在這裏面組裝了操做數,見代碼:
static int lookup_cv(zend_op_array *op_array, zend_string* name) /* {{{ */{ int i = 0; zend_ulong hash_value = zend_string_hash_val(name); //判斷變量是否在vars中存在,若存在直接返回對應的位置 while (i < op_array->last_var) { if (ZSTR_VAL(op_array->vars[i]) == ZSTR_VAL(name) || (ZSTR_H(op_array->vars[i]) == hash_value && ZSTR_LEN(op_array->vars[i]) == ZSTR_LEN(name) && memcmp(ZSTR_VAL(op_array->vars[i]), ZSTR_VAL(name), ZSTR_LEN(name)) == 0)) { zend_string_release(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); } i++; } //若不存在,則寫入vars中,返回新插入的位置 i = op_array->last_var; op_array->last_var++; /*…代碼省略…*/ op_array->vars[i] = zend_new_interned_string(name); return (int)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, i); }
從代碼中能夠看出,變量是存放到op_array->vars中的,而返回的是一個int型的地址,這個是什麼呢?咱們看一下宏ZEND_CALL_VAR_NUM的定義:
#define ZEND_CALL_VAR_NUM(call, n) \ (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n)))) #define ZEND_CALL_FRAME_SLOT \ ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
能夠看出,這個值都是sizeof(zval)的整數倍,在筆者的機器上,zval的大小爲16,而zend_execute_data大小爲80,因此返回的是每一個變量的偏移值,即80+16*i,如圖12所示。
圖12 左子女var_ast編譯示意圖
此時,就將賦值語句$a=1中,左側表達式$a編譯完成,賦值給了znode* result,下面繼續對右子女常量1的編譯。
void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */ { /* CG(zend_lineno) = ast->lineno; */ CG(zend_lineno) = zend_ast_get_lineno(ast); switch (ast->kind) { case ZEND_AST_ZVAL: ZVAL_COPY(&result->u.constant, zend_ast_get_zval(ast)); result->op_type = IS_CONST; return;
從代碼中能夠看出,對於常量1,經過ZVAL_COPY,將值複製到result->u.constan中,同時將result->op_type賦值爲IS_CONST。這樣,對於assign操做的兩個操做數都編譯完成了,下面咱們看一下對應指令opline的生成過程。
static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */ { //分配和獲取opline,並設置其opcode zend_op *opline = get_next_op(CG(active_op_array)); opline->opcode = opcode; //設置操做數1 if (op1 == NULL) { SET_UNUSED(opline->op1); } else { SET_NODE(opline->op1, op1); } //設置操做數2 if (op2 == NULL) { SET_UNUSED(opline->op2); } else { SET_NODE(opline->op2, op2); } zend_check_live_ranges(opline); if (result) { //設置返回值 zend_make_var_result(result, opline); } return opline; }
其中對操做數獲得設置,對應的是宏SET_NODE,見代碼:
#define SET_NODE(target, src) do { \ target ## _type = (src)->op_type; \ if ((src)->op_type == IS_CONST) { \ target.constant = zend_add_literal(CG(active_op_array), &(src)->u.constant); \ } else { \ target = (src)->u.op; \ } \ } while (0) int zend_add_literal(zend_op_array *op_array, zval *zv) /* {{{ */ { int i = op_array->last_literal; op_array->last_literal++; if (i >= CG(context).literals_size) { while (i >= CG(context).literals_size) { CG(context).literals_size += 16; /* FIXME */ } op_array->literals = (zval*)erealloc(op_array->literals, CG(context).literals_size * sizeof(zval)); } zend_insert_literal(op_array, zv, i); return i; }
從代碼中能夠看出,對於操做數1,會將編譯過程當中臨時的結構znode傳遞給zend_op中,對於操做數2,由於是常量(IS_CONST),會調用zend_add_literal將其插入到op_array->literals中。
從返回值的設置,調用的是zend_make_var_result,其代碼以下:
static inline void zend_make_var_result(znode *result, zend_op *opline) /* {{{ */ { //返回值的類型設置爲IS_VAR opline->result_type = IS_VAR; //這個是返回值的編號,對應T位置 opline->result.var = get_temporary_variable(CG(active_op_array)); GET_NODE(result, opline->result); } static uint32_t get_temporary_variable(zend_op_array *op_array) /* {{{ */ { return (uint32_t)op_array->T++; }
返回值的類型爲IS_VAR,result.var爲T的值,下面咱們給出Assign操做對應的指令圖,如圖13所示。
圖13 Assign指令示意圖
從圖13中能夠看出,生成的opline中opcode等於38;op1的類型爲IS_CV,op1.var對應的是vm_stack上的偏移量;op2的類型爲IS_CONST,op2.constant對應的是op_array中literals數組的下標;result的類型爲IS_VAR,result.var對應的是T的值;此時handler的值爲空。
對於「$b =$a+2;」語句,首先是add語句,也就是$a+1,跟assign語句類型相似,不一樣是調用了函數zend_compile_binary_op,見代碼:
void zend_compile_binary_op(znode *result, zend_ast *ast) /* {{{ */ { zend_ast *left_ast = ast->child[0]; zend_ast *right_ast = ast->child[1]; uint32_t opcode = ast->attr;//經過attr區分加減乘除等等操做 znode left_node, right_node; zend_compile_expr(&left_node, left_ast); zend_compile_expr(&right_node, right_ast); /*…代碼省略…*/ zend_emit_op_tmp(result, opcode, &left_node, &right_node); /*…代碼省略…*/ }
對於加減乘除等操做,kind都是ZEND_AST_BINARY_OP,具體操做經過AST中的attr區分的,由於$a+1會生成臨時變量,所以與Assign操做不一樣,調用的函數是zend_emit_op_tmp:
static zend_op *zend_emit_op_tmp(znode *result, zend_uchar opcode, znode *op1, znode *op2) /* {{{ */ { /*…代碼與zend_emit_op同樣…*/ if (result) { zend_make_tmp_result(result, opline); } return opline; }
zend_emit_op_tmp函數與zend_emit_op相似,opline中的操做數1和操做數2作了一樣的操做,而result不一樣之處在於,其類型是IS_TMP_VAR,所以opline如圖14所示。
圖14 Add指令示意圖
對於「$b=$a+2;」至關於把臨時變量賦值給$b,與Assign編譯過程一致,生成opline如圖15所示。
圖15 第2條Assign指令示意圖
void zend_compile_echo(zend_ast *ast) /* {{{ */ { zend_op *opline; zend_ast *expr_ast = ast->child[0]; znode expr_node; zend_compile_expr(&expr_node, expr_ast); opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL); opline->extended_value = 0; }
Echo對應的指令只有一個操做數,對於操做數2,SET_UNUSED宏設置爲IS_UNUSED。
#define SET_UNUSED(op) op ## _type = IS_UNUSED
生成的opline如圖16所示。
圖16 Echo指令示意圖
上面對於AST編譯並無結束,PHP代碼中雖然沒有return操做,可是默認會生成一條ZEND_RETURN指令,經過zend_emit_final_return含設置,代碼以下:
void zend_emit_final_return(int return_one) /* {{{ */ { znode zn; zend_op *ret; /**代碼省略**/ zn.op_type = IS_CONST; if (return_one) { ZVAL_LONG(&zn.u.constant, 1); } else { ZVAL_NULL(&zn.u.constant); } ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); ret->extended_value = -1; }
一樣經過zend_emit_op設置opline,設置之後的opline如圖17所示。
圖17 Return指令示意圖
通過對Assign、Add和Echo的編譯後,生成的所有oplines如圖18所示。
圖18 全部指令集示意圖
到這裏,咱們瞭解了AST編譯生成opline指令集的過程,包括op一、op2和result的生成過程,可是此時opline中的handler還都是空指針,接下來咱們看一下handler設置的過程。
抽象語法樹編譯後還有一個重要操做,函數叫pass_two,這個函數中,對opline指令集作了進一步的加工,最主要的工做是設置指令的handler,代碼以下:
ZEND_API int pass_two(zend_op_array *op_array) { /**代碼省略**/ while (opline < end) {//遍歷opline數組 if (opline->op1_type == IS_CONST) { ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1); } else if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) { opline->op1.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op1.var); } if (opline->op2_type == IS_CONST) { ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op2); } else if (opline->op2_type & (IS_VAR|IS_TMP_VAR)) { opline->op2.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->op2.var); } if (opline->result_type & (IS_VAR|IS_TMP_VAR)) { opline->result.var = (uint32_t)(zend_intptr_t)ZEND_CALL_VAR_NUM(NULL, op_array->last_var + opline->result.var); } ZEND_VM_SET_OPCODE_HANDLER(opline); /**代碼省略**/ }
從代碼中能夠看出,該函數會對opline指令數組進行遍歷,對每一條opline指令進行操做,對於op1和op2若是是IS_CONST類型,調用ZEND_PASS_TWO_UPDATE_CONSTANT,見代碼:
/* convert constant from compile-time to run-time */ # define ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, node) do { \ (node).constant *= sizeof(zval); \ } while (0)
根據上一節的知識咱們知道,對於IS_CONST類型的變量,其值是存在op_array->literals數組中,所以,能夠直接使用數組下標乘以sizeof(zval)轉換爲偏移量。
對於op1和op2若是是IS_VAR或者IS_TMP_VAR類型的變量,跟上一節同樣,經過ZEND_CALL_VAR_NUM計算偏移量。
另一個很是重要的工做是經過ZEND_VM_SET_OPCODE_HANDLER(opline),設置opline對應的hanlder,代碼以下:
ZEND_API void zend_vm_set_opcode_handler(zend_op* op) { op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op); }
其中opcode和handler以前的對應關係在Zend/zend_vm_execute.h中定義的。opline數組通過一次遍歷後,handler也就設置完畢,設置後的opline數組如圖19所示。
圖19 設置handler後的指令集
到此,整個抽象語法樹就編譯完成了,最終的結果爲opline指令集,接下來就是在Zend虛擬機上執行這些指令。
執行的入口函數爲zend_execute,在該函數中會針對上一節生成的opline指令集進行調度執行。首先會在EG(vm_stack)上分配空間,而後每一條指令依次壓棧並調用對應的handler。代碼以下:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) { zend_execute_data *execute_data; /**代碼省略**/ //壓棧生成execute_data execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE, (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data))); //設置symbol_table if (EG(current_execute_data)) { execute_data->symbol_table = zend_rebuild_symbol_table(); } else { execute_data->symbol_table = &EG(symbol_table); } EX(prev_execute_data) = EG(current_execute_data); //初始化execute_data i_init_execute_data(execute_data, op_array, return_value); //執行 zend_execute_ex(execute_data); //釋放execute_data zend_vm_stack_free_call_frame(execute_data); }
這個代碼中首先根據op_array中的指令生成對應的execute_data,而後初始化後調用handler執行。下面咱們具體分析一下執行的過程。
執行棧是經過2.6節介紹的zend_vm_stack_push_call_frame完成的,會在EG(vm_stack)上分配一塊內存區域,80個字節用來存放execute_data,緊接着下面是根據last_var和T的數量分配zval大小的空間,以3節編譯生成的指令集爲例,分配的棧如圖20所示。
圖20 執行棧分配示意圖
從圖20中看出,在EG(vm_stack)上分配空間,空間的大小跟op_array中last_var和T的值相關。
在執行棧上分配空間後,會調用函數i_init_execute_data對執行數據進行初始化,見代碼:
static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value) /* {{{ */ { ZEND_ASSERT(EX(func) == (zend_function*)op_array); EX(opline) = op_array->opcodes;//讀取第一條指令 EX(call) = NULL; EX(return_value) = return_value;//設置返回值 if (EX_CALL_INFO() & ZEND_CALL_HAS_SYMBOL_TABLE) { //賦值符號表 zend_attach_symbol_table(execute_data); /**代碼省略**/ //運行時緩存 if (!op_array->run_time_cache) { if (op_array->function_name) { op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size); } else { op_array->run_time_cache = emalloc(op_array->cache_size); } memset(op_array->run_time_cache, 0, op_array->cache_size); } EX_LOAD_RUN_TIME_CACHE(op_array); EX_LOAD_LITERALS(op_array);//設置常量數組 EG(current_execute_data) = execute_data; }
從代碼中能夠看出,初始化工做主要作了幾件事:
作完這些工做後,執行棧中數據的結果如圖21所示。
圖21 初始化execute_data示意圖
接下來是調用execute_ex進行指令的執行,見代碼:
ZEND_API void execute_ex(zend_execute_data *ex) { ZEND_VM_LOOP_INTERRUPT_CHECK(); while (1) { //循環 int ret; if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) { if (EXPECTED(ret > 0)) { execute_data = EG(current_execute_data); ZEND_VM_LOOP_INTERRUPT_CHECK(); } else { return; } } }
從代碼中能夠看出,整個執行最外層是while循環,直到結束才退出。調用的是opline中對應的handler,下面以3節中生成的指令集進行詳細的闡述。
//ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER //經過op2獲取到常量數組裏面的值 value = EX_CONSTANT(opline->op2); //獲取到op1對應的位置 variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); //將常量賦值給對應位置的指針 value = zend_assign_to_variable(variable_ptr, value, IS_CONST); //將結果複製到result ZVAL_COPY(EX_VAR(opline->result.var), value); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
首先經過op2.constant值獲取到常量表中的zval值,經過op1.var獲取到棧中對應的位置,而後將常量值賦值到對應的位置,同時將其copy到result對應的位置,如圖22所示。
圖22 Assign指令執行示意圖
完成assign操做後,會調用ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION宏進行下一條指令的執行,也就是opline+1。
//ZEND_ADD_SPEC_CV_CONST_HANDLER zval *op1, *op2, *result; //獲取op1對應的位置 op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var); //獲取op2對應的值 op2 = EX_CONSTANT(opline->op2); /**代碼省略**/ //執行相加操做,賦值給result add_function(EX_VAR(opline->result.var), op1, op2); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
首先根據op1.var獲取對應的位置,而後根據op2.constant值獲取到常量表中的zval值,最後進行相加操做,賦值給result對應的位置,如圖23所示。
圖23 Add指令執行示意圖
//ZEND_ASSIGN_SPEC_CV_TMP_RETVAL_UNUSED_HANDLER zval *value; zval *variable_ptr; //根據op2.var獲取臨時變量的位置 value = _get_zval_ptr_tmp(opline->op2.var, execute_data, &free_op2); //根據op1.var獲取操做數1 的位置 variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); //將臨時變量賦值給操做數1對應的位置 value = zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR); //同時拷貝到result對應的位置 ZVAL_COPY(EX_VAR(opline->result.var), value); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
與第一條指令相似,執行過程如圖24所示。
圖24 第2條Assign指令示意圖
// ZEND_ECHO_SPEC_CV_HANDLER zval *z; //根據op1.var獲取對應位置的值 z = _get_zval_ptr_cv_undef(execute_data, opline->op1.var); //調用zend_write輸出 zend_write(ZSTR_VAL(str), ZSTR_LEN(str)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
這條指令會根據op1.var獲取到對應的位置,取出zval值輸出,如圖25所示。
圖25 Echo指令執行示意圖
//ZEND_RETURN_SPEC_CONST_HANDLER zval *retval_ptr; zval *return_value; retval_ptr = EX_CONSTANT(opline->op1); return_value = EX(return_value); //調用zend_leave_helper_SPEC函數,返回 ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
這條指令沒有作實質性的操做,核心是返回-1,讓while循環退出,指令執行結束。
到此,整個的執行過程就闡述完成了,相信讀者經過這五條指令的執行,初步理解了Zend虛擬機的執行過程。
指令執行完畢後,調用zend_vm_stack_free_call_frame釋放execute_data,並回收EG(vm_stack)上使用的空間,這部分比較簡單。
本文主要介紹了Zend虛擬機的實現原理,包括抽象語法樹編譯生成指令集的過程,以及指令集執行的過程。同時介紹了Zend虛擬機運行中用到的數據結構。但願讀者讀完本文,可以對Zend虛擬機有必定的認識。