1.從物理機說起
虛擬機也是計算機,設計思想和物理機有很多相似之處;
1.1馮諾依曼體系結構
馮·諾依曼是當之無愧的數字計算機之父,當前計算機都采用的是馮諾依曼體系結構;設計思想主要包含以下幾個方面:
指令和數據不加區別混合存儲在同一個存儲器中,它們都是內存中的數據?,F代CPU的保護模式,每個內存段都有段描述符,這個描述符記錄著這個內存段的訪問權限(可讀,可寫,可執行)。這就變相的指定了哪些內存中存儲的是指令哪些是數據);
存儲器是按地址訪問的線性編址的一維結構,每個單元的位數是固定的;
數據以二進制表示;
指令由操作碼和操作數組成。操作碼指明本指令的操作類型,操作數指明操作數本身或者操作數的地址。操作數本身并無數據類型,它的數據類型由操作碼確定;任何架構的計算機都會對外提供指令集合;
運算器通過執行指令直接發出控制信號控制計算機各項操作。由指令計數器指明待執行指令所在的內存地址。指令計數器只有一個,一般按順序遞增,但執行順序可能因為運算結果或當時的外界條件而改變;
1.2匯編語言簡介
任何架構的計算機都會提供一組指令集合;
指令由操作碼和操作數組成;操作碼即操作類型,操作數可以是一個立即數或者一個存儲地址;每條指令可以有0、1或2個操作數;
指令就是一串二進制;匯編語言是二進制指令的文本形式;
push %ebxmov %eax, [%esp+8]mov %ebx, [%esp+12]add %eax, %ebxpop %ebx
push、mov、add、pop等就是操作碼;
%ebx寄存器;[%esp+12]內存地址;
操作數只是一塊可存取數據的存儲區;操作數本身并無數據類型,它的數據類型由操作碼確定;
如movb傳送字節,movw傳送字,movl傳送雙字等
1.3 函數調用棧
過程(函數)是對代碼的封裝,對外暴露的只是一組指定的參數和一個可選的返回值;可以在程序中不同的地方調用這個函數;假設過程P調用過程Q,Q執行后返回過程P;為了實現這一功能,需要考慮三點:
指令跳轉:進入過程Q的時候,程序計數器必須被設置為Q的代碼的起始地址;在返回時,程序計數器需要設置為P中調用Q后面那條指令的地址;
數據傳遞:P能夠向Q提供一個或多個參數,Q能夠向P返回一個值;
內存分配與釋放:Q開始執行時,可能需要為局部變量分配內存空間,而在返回前,又需要釋放這些內存空間;
大多數的語言過程調用都采用了棧數據結構提供的內存管理機制;如下圖所示:
函數的調用與返回即對應的是一系列的入棧與出棧操作;
函數在執行時,會有自己私有的棧幀,局部變量就是分配在函數私有棧幀上的;
平時遇到的棧溢出就是因為調用函數層級過深,不斷入棧導致的;
2.PHP虛擬機
虛擬機也是計算機,參考物理機的設計,設計虛擬機時,首先應該考慮三個要素:指令,數據存儲,函數棧幀;
下面從這三點詳細分析PHP虛擬機的設計思路;
2.1指
2.1.1 指令類型
任何架構的計算機都需要對外提供一組指令集,其代表計算機支持的一組操作類型;
PHP虛擬機對外提供186種指令,定義在zend_vm_opcodes.h文件中;
//加、減、乘、除等#define ZEND_ADD 1#define ZEND_SUB 2#define ZEND_MUL 3#define ZEND_p 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……………………
2.1.2 指令
2.1.2.1指令的表示
指令由操作碼和操作數組成;操作碼指明本指令的操作類型,操作數指明操作數本身或者操作數的地址;
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_html' target='_blank'>value;//擴展 uint32_t lineno; //行號 zend_uchar opcode; //指令類型 zend_uchar op1_type; //操作數1的類型(此類型并不代表字符串、數組等數據類型;其表示此操作數是常量,臨時變量,編譯變量等) zend_uchar op2_type; //操作數2的類型 zend_uchar result_type; //返回值的類型};
2.1.2.2 操作數的表示
從上面可以看到,操作數使用結構體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;
2.2 數據存儲
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.3 再談指令
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();}
2.4 函數棧幀
2.4.1指令集
上面分析了指令的結構與表示,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的值設置為該變量的操作數;如果存在,則使用之前分配的操作數
2.4.2 函數棧幀
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大?。?0字節/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;}
從上面分析可以得到函數棧幀結構圖如下所示:
總結
PHP虛擬機也是計算機,有三點是我們需要重點關注的:指令集(包含指令處理函數)、數據存儲(zval)、函數棧幀;
此時虛擬機已可以接受指令并執行指令代碼;
但是,PHP虛擬機是專用執行PHP代碼的,PHP代碼如何能轉換為PHP虛擬機可以識別的指令呢——編譯;
PHP虛擬機同時提供了編譯器,可以將PHP代碼轉換為其可以識別的指令集合;
理論上你可以自定義任何語言,只要實現編譯器,能夠將你自己的語言轉換為PHP可以識別的指令代碼,就能被PHP虛擬機執行;
相關文章推薦:
PHP7.0和php7.1中的語法新特性的總結
PHP中如何將session存入數據庫并使用(附代碼)
PHP中時間函數strtotime() 函數的原理講解
以上就是PHP7源碼:PHP虛擬機的詳細解析的詳細內容,PHP教程
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。
新聞熱點
疑難解答