本篇文章給大家帶來的內容是關於PHP7源碼:PHP虛擬機器的詳細解析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。
1.從物理機說起
虛擬機器也是電腦,設計思想和物理機有很多相似之處;
1.1馮諾依曼體繫結構
馮·諾依曼是當之無愧的數字電腦之父,當前電腦都採用的是馮諾依曼體繫結構;設計思想主要包含以下幾個方面:
指令和資料不加區別混合儲存在同一個儲存空間中,它們都是記憶體中的資料。現代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_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大小(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;}
從上面分析可以得到函數棧幀結構圖如下所示:
總結
PHP虛擬機器也是電腦,有三點是我們需要重點關注的:指令集(包含指令處理函數)、資料存放區(zval)、函數棧幀;
此時虛擬機器已可以接受指令並執行指令代碼;
但是,PHP虛擬機器是專用執行PHP代碼的,PHP代碼如何能轉換為PHP虛擬機器可以識別的指令呢——編譯;
PHP虛擬機器同時提供了編譯器,可以將PHP代碼轉換為其可以識別的指令集合;
理論上你可以自訂任何語言,只要實現編譯器,能夠將你自己的語言轉換為PHP可以識別的指令代碼,就能被PHP虛擬機器執行;
相關文章推薦:
PHP7.0和php7.1中的文法新特性的總結
PHP中如何將session存入資料庫並使用(附代碼)
PHP中時間函數strtotime() 函數的原理講解