順風車運營研發團隊 李樂node
虛擬機也是計算機,設計思想和物理機有不少類似之處;數組
馮·諾依曼是當之無愧的數字計算機之父,當前計算機都採用的是馮諾依曼體系結構;設計思想主要包含如下幾個方面:數據結構
任何架構的計算機都會提供一組指令集合;架構
指令由操做碼和操做數組成;操做碼即操做類型,操做數能夠是一個當即數或者一個存儲地址;每條指令能夠有0、1或2個操做數;函數
指令就是一串二進制;彙編語言是二進制指令的文本形式;ui
push %ebx mov %eax, [%esp+8] mov %ebx, [%esp+12] add %eax, %ebx pop %ebx
push、mov、add、pop等就是操做碼;
%ebx寄存器;[%esp+12]內存地址;
操做數只是一塊可存取數據的存儲區;操做數自己並沒有數據類型,它的數據類型由操做碼肯定;
如movb傳送字節,movw傳送字,movl傳送雙字等this
過程(函數)是對代碼的封裝,對外暴露的只是一組指定的參數和一個可選的返回值;能夠在程序中不一樣的地方調用這個函數;假設過程P調用過程Q,Q執行後返回過程P;爲了實現這一功能,須要考慮三點:spa
大多數的語言過程調用都採用了棧數據結構提供的內存管理機制;以下圖所示:設計
函數的調用與返回即對應的是一系列的入棧與出棧操做;
函數在執行時,會有本身私有的棧幀,局部變量就是分配在函數私有棧幀上的;
平時遇到的棧溢出就是由於調用函數層級過深,不斷入棧致使的;指針
虛擬機也是計算機,參考物理機的設計,設計虛擬機時,首先應該考慮三個要素:指令,數據存儲,函數棧幀;
下面從這三點詳細分析PHP虛擬機的設計思路;
任何架構的計算機都須要對外提供一組指令集,其表明計算機支持的一組操做類型;
PHP虛擬機對外提供186種指令,定義在zend_vm_opcodes.h文件中;
//加、減、乘、除等 #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_SR 7 #define ZEND_CONCAT 8 #define ZEND_BW_OR 9 #define ZEND_BW_AND 10 ……………………
指令由操做碼和操做數組成;操做碼指明本指令的操做類型,操做數指明操做數自己或者操做數的地址;
PHP虛擬機定義指令格式爲:操做碼 操做數1 操做數2 返回值;其使用結構體_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; //指令類型 zend_uchar op1_type; //操做數1的類型(此類型並不表明字符串、數組等數據類型;其表示此操做數是常量,臨時變量,編譯變量等) zend_uchar op2_type; //操做數2的類型 zend_uchar result_type; //返回值的類型 };
從上面能夠看到,操做數使用結構體znode_op表示,定義以下:
constant、var、num等都是uint32_t類型的,這怎麼表示一個操做數呢?(既不是指針不能表明地址,也沒法表示全部數據類型);
其實,操做數大多狀況採用的相對地址表示方式,constant等表示的是相對於執行棧幀首地址的偏移量;
另外,_znode_op結構體中有個zval *zv字段,其也能夠表示一個操做數,這個字段是一個指針,指向的是zval結構體,PHP虛擬機支持的全部數據類型都使用zval結構體表示;
typedef union _znode_op { uint32_t constant; uint32_t var; uint32_t num; uint32_t opline_num; #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;
PHP虛擬機支持多種數據類型:整型、浮點型、字符串、數組,對象等;PHP虛擬機如何存儲和表示多種數據類型?
2.1.2.2節指出結構體_znode_op表明一個操做數;操做數能夠是一個偏移量(計算獲得一個地址,即zval結構體的首地址),或者一個zval指針;PHP虛擬機使用zval結構體表示和存儲多種數據;
struct _zval_struct { zend_value value; //存儲實際的value值 union { struct { //一些標誌位 ZEND_ENDIAN_LOHI_4( zend_uchar type, //重要;表示變量類型 zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { //其餘有用信息 uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ } u2; };
zval.u1.type表示數據類型, zend_types.h文件定義瞭如下類型:
#define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 …………
zend_value存儲具體的數據內容,結構體定義以下:
_zend_value佔16字節內存;long、double類型會直接存儲在結構體;引用、字符串、數組等類型使用指針存儲;
代碼中根據zval.u1.type字段,判斷數據類型,以此決定操做_zend_value結構體哪一個字段;
能夠看出,字符串使用zend_string表示,數組使用zend_array表示…
typedef union _zend_value { zend_long lval; double dval; zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value;
以下圖爲PHP7中字符串結構圖:
2.1.2.1指出,指令使用結構體_zend_op表示;其中最主要2個屬性:操做函數,操做數(兩個操做數和一個返回值);
操做數的類型(常量、臨時變量等)不一樣,同一個指令對應的handler函數也會不一樣;操做數類型定義在 Zend/zend_compile.h文件:
//常量 #define IS_CONST (1<<0) //臨時變量,用於操做的中間結果;不能被其餘指令對應的handler重複使用 #define IS_TMP_VAR (1<<1) //這個變量並非PHP代碼中聲明的變量,常見的是返回的臨時變量,好比$a=time(), 函數time返回值的類型就是IS_VAR,這種類型的變量是能夠被其餘指令對應的handler重複使用的 #define IS_VAR (1<<2) #define IS_UNUSED (1<<3) /* Unused variable */ //編譯變量;即PHP中聲明的變量; #define IS_CV (1<<4) /* Compiled variable */
操做函數命名規則爲:ZEND_[opcode]_SPEC_(操做數1類型)_(操做數2類型)_(返回值類型)_HANDLER
好比賦值語句就有如下多種操做函數:
ZEND_ASSIGN_SPEC_VAR_CONST_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER, ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER, …
對於$a=1,其操做函數爲: 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(execute_data相似函數棧幀,後面詳細分析) 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); if (UNEXPECTED(0)) { ZVAL_COPY(EX_VAR(opline->result.var), value); } ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); }
上面分析了指令的結構與表示,PHP虛擬機使用_zend_op_array表示指令的集合:
struct _zend_op_array { ………… //last表示指令總數;opcodes爲存儲指令的數組; uint32_t last; zend_op *opcodes; //變量類型爲IS_CV的個數 int last_var; //變量類型爲IS_VAR和IS_TEMP_VAR的個數 uint32_t T; //存放IS_CV類型變量的數組 zend_string **vars; ………… //靜態變量 HashTable *static_variables; //常量個數;常量數組 int last_literal; zval *literals; … };
注意: last_var表明IS_CV類型變量的個數,這種類型變量存放在vars數組中;在整個編譯過程當中,每次遇到一個IS_CV類型的變量(相似於$something),就會去遍歷vars數組,檢查是否已經存在,若是不存在,則插入到vars中,並將last_var的值設置爲該變量的操做數;若是存在,則使用以前分配的操做數
PHP虛擬機實現了與1.3節物理機相似的函數棧幀結構;
使用 _zend_vm_stack表示棧結構;多個棧之間使用prev字段造成單向鏈表;top和end指向棧低和棧頂,分別爲zval類型的指針;
struct _zend_vm_stack { zval *top; zval *end; zend_vm_stack prev; };
考慮如何設計函數執行時候的幀結構:當前函數執行時,須要存儲函數編譯後的指令,須要存儲函數內部的局部變量等(2.1.2.2節指出,操做數使用結構體znode_op表示,其內部使用uint32_t表示操做數,此時表示的就是當前zval變量相對於當前函數棧幀首地址的偏移量);
PHP虛擬機使用結構體_zend_execute_data存儲當前函數執行所需數據;
struct _zend_execute_data { //當前指令指令 const zend_op *opline; //當前函數執行棧幀 zend_execute_data *call; //函數返回數據 zval *return_value; zend_function *func; zval This; /* this + call_info + num_args */ //調用當前函數的棧幀 zend_execute_data *prev_execute_data; //符號表 zend_array *symbol_table; #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; #endif #if ZEND_EX_USE_LITERALS //常量數組 zval *literals; #endif };
函數開始執行時,須要爲函數分配相應的函數棧幀併入棧,代碼以下:
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); } //計算函數棧幀大小 static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func) { //_zend_execute_data大小(80字節/16字節=5)+參數數目 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); } //乘以16字節 return used_stack * sizeof(zval); } //入棧 static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object) { //上一個函數棧幀地址 zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top); //移動函數調用棧top指針 EG(vm_stack_top) = (zval*)((char*)call + used_stack); //初始化當前函數棧幀 zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object); //返回當前函數棧幀首地址 return call; }
從上面分析能夠獲得函數棧幀結構圖以下所示: