摘要:操作數本身并無數據類型,它的數據類型由操作碼確定任何架構的計算機都會對外提供指令集合運算器通過執行指令直接發出控制信號控制計算機各項操作。
順風車運營研發團隊 李樂
1.從物理機說起虛擬機也是計算機,設計思想和物理機有很多相似之處;
1.1馮諾依曼體系結構馮·諾依曼是當之無愧的數字計算機之父,當前計算機都采用的是馮諾依曼體系結構;設計思想主要包含以下幾個方面:
指令和數據不加區別混合存儲在同一個存儲器中,它們都是內存中的數據?,F代CPU的保護模式,每個內存段都有段描述符,這個描述符記錄著這個內存段的訪問權限(可讀,可寫,可執行)。這就變相的指定了哪些內存中存儲的是指令哪些是數據);
存儲器是按地址訪問的線性編址的一維結構,每個單元的位數是固定的;
數據以二進制表示;
指令由操作碼和操作數組成。操作碼指明本指令的操作類型,操作數指明操作數本身或者操作數的地址。操作數本身并無數據類型,它的數據類型由操作碼確定;任何架構的計算機都會對外提供指令集合;
運算器通過執行指令直接發出控制信號控制計算機各項操作。由指令計數器指明待執行指令所在的內存地址。指令計數器只有一個,一般按順序遞增,但執行順序可能因為運算結果或當時的外界條件而改變;
1.2匯編語言簡介任何架構的計算機都會提供一組指令集合;
指令由操作碼和操作數組成;操作碼即操作類型,操作數可以是一個立即數或者一個存儲地址;每條指令可以有0、1或2個操作數;
指令就是一串二進制;匯編語言是二進制指令的文本形式;
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傳送雙字等
過程(函數)是對代碼的封裝,對外暴露的只是一組指定的參數和一個可選的返回值;可以在程序中不同的地方調用這個函數;假設過程P調用過程Q,Q執行后返回過程P;為了實現這一功能,需要考慮三點:
指令跳轉:進入過程Q的時候,程序計數器必須被設置為Q的代碼的起始地址;在返回時,程序計數器需要設置為P中調用Q后面那條指令的地址;
數據傳遞:P能夠向Q提供一個或多個參數,Q能夠向P返回一個值;
內存分配與釋放:Q開始執行時,可能需要為局部變量分配內存空間,而在返回前,又需要釋放這些內存空間;
大多數的語言過程調用都采用了棧數據結構提供的內存管理機制;如下圖所示:
函數的調用與返回即對應的是一系列的入棧與出棧操作;
函數在執行時,會有自己私有的棧幀,局部變量就是分配在函數私有棧幀上的;
平時遇到的棧溢出就是因為調用函數層級過深,不斷入棧導致的;
虛擬機也是計算機,參考物理機的設計,設計虛擬機時,首先應該考慮三個要素:指令,數據存儲,函數棧幀;
下面從這三點詳細分析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_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 ……………………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虛擬機執行;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/29184.html
摘要:在中,源代碼首先將進行詞法分析,將源代碼切割為多個字符串單元,分割后的字符串稱之為。圖以為例解釋型語言的執行示意圖第步源碼通過詞法分析得到第步基于語法分析器生成抽象語法樹第步抽象語法樹轉換為指令集合,解釋執行。 順風車運營研發團隊 李志 發表在程序人生 公眾號我們常用的高級語言有很多種,比較出名的有CC++、Python、 PHP、Go、Pascal等。而這些語言根據運行的方式不同,...
摘要:中詞法語法分析,生成抽象語法樹,然后編譯成及被執行均由虛擬機完成。通常情況下這部分是可選部分,主要為便于程序的讀寫方便而使用。指令虛擬機的指令稱為,每條指令對應一個。 作者 陳雷編程語言的虛擬機是一種可以運行中間語言的程序。中間語言是抽象出的指令集,由原生語言編譯而成,作為虛擬機執行階段的輸入。很多語言都實現了自己的虛擬機,比如Java、C#和Lua。PHP語言也有自己的虛擬機,稱為Z...
摘要:我們修改上面代碼,再來看下返回值類型限制的情況運行結果這段代碼我們額外聲明了返回值的類型為型。對函數返回值的聲明做了擴充,可以定義其返回值為,無論是否開啟嚴格模式,只要函數中有以外的其他語句都會報錯。 順風車運營研發團隊 王坤 發表至21CTO公眾號(https://mp.weixin.qq.com/s/ph...) showImg(https://segmentfault.c...
閱讀 1958·2021-11-22 15:33
閱讀 3001·2021-11-18 10:02
閱讀 2602·2021-11-08 13:16
閱讀 1617·2021-10-09 09:57
閱讀 1366·2021-09-30 09:47
閱讀 2000·2019-08-29 13:05
閱讀 3064·2019-08-29 12:46
閱讀 1004·2019-08-29 12:19