顺风车运营研发团队 李乐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; }
从上面分析能够获得函数栈帧结构图以下所示: