顺风车运维研发团队李乐1.从物理机来说,虚拟机也是一台电脑,设计思路与物理有很多相似之处机器;1.1FengNoy冯诺依曼架构冯诺依曼是当之无愧的数字计算机之父,目前的计算机采用冯诺依曼架构;其设计思想主要包括以下几个方面:指令和数据不加区分地混合存储在同一内存中,它们都是内存中的数据。在现代CPU的保护模式下,每个内存段都有一个段描述符,段描述符记录了这个内存段的访问权限(可读、可写、可执行)。这变相地指定了哪些内存存储指令,哪些是数据);存储器是按地址访问的线性寻址的一维结构,每个单元的位数是固定的;数据以二进制表示;指令由操作码和操作数表示。操作码表示指令的操作类型,操作数表示操作数本身或操作数的地址。操作数本身没有数据类型,其数据类型由操作码决定;任何具有任何体系结构的计算机都会提供一组指令;运算器直接发出控制信号,通过执行指令来控制计算机的各种运算。要执行的指令所在的内存地址由指令计数器指示。指令计数器只有一个,一般按顺序递增,但执行顺序可能会因运算结果或当时的外界条件而改变;1.2汇编语言简介任何体系结构的计算机都会提供一套指令;指令由操作码和操作数组成;operationscode是操作类型,操作数可以是立即数,也可以是存储地址;每条指令可以有0、1或2个操作数;该指令是一串二进制;汇编语言是二进制指令的文本形式;push%ebxmov%eax、[%esp+8]mov%ebx、[%esp+12]add%eax、%ebxpop%ebxpush、mov、add、pop等都是操作码;%ebx寄存器;[%esp+12]内存地址;操作数只是一个可以访问数据的存储区;操作数本身没有数据类型,其数据类型由操作码决定;例如movb传输字节,movw传输字,movl传输双字等。1.3函数调用栈过程(Function)是对代码的封装,对外只暴露一组指定的参数和一个可选的返回值世界;这个函数可以在程序的不同地方调用;假设进程P调用进程Q,Q执行后返回给进程P;为了实现这个功能,需要考虑三点:指令跳转:进入进程Q时,程序计数器必须设置为Q的代码的起始地址;一条指令的地址;数据传递:P可以向Q提供一个或多个参数,Q可以返回一个值给P;内存分配和释放:当Q开始执行时,可能需要为局部变量分配内存空间,在返回之前,需要释放这些内存空间;大多数语言过程调用使用堆栈数据结构提供的内存管理机制;如下图所示:函数调用和返回对应一系列的push和pop操作;一个函数在执行时,会有自己的私有栈帧,局部变量分配在函数的私有栈帧上;经常遇到的栈溢出是调用函数太深,不断压入栈造成的;2、PHP虚拟机虚拟机也是一台电脑。参考物理机的设计,在设计虚拟机时,首先要考虑三个要素:指令、数据存储、函数栈帧;下面就从这三点来详细分析一下PHP虚拟机的设计思路;2.1说明2.1。1指令类型任何体系结构的计算机都需要提供一组指令集,指令集代表计算机支持的一组操作类型;PHP虚拟机提供了186条指令,定义在zend_vm_opcodes.h文件中;//加法、减法、乘法、除法等#defineZEND_ADD1#defineZEND_SUB2#defineZEND_MUL3#defineZEND_DIV4#defineZEND_MOD5#defineZEND_SL6#defineZEND_SR7#defineZEND_CONCAT8#defineZEND_BW_OR9#defineZEND_BW_AND10…………………………2.1.2使用说明2.1.2.1使用说明指令由操作码和操作数组成;操作码表示指令的操作类型,操作数表示操作数本身或操作数的地址;PHP虚拟机定义指令格式为:opcodeoperand1operand2returnvalue;它使用结构_zend_op来表示一条指令:struct_zend_op{constvoid*handler;//指向当前指令的执行函数指针znode_opop1;//操作数1znode_opop2;//操作数2znode_op结果;//返回值uint32_textended_value;//扩展uint32_tlineno;//行号zend_ucharopcode;//指令类型zend_ucharop1_type;//操作数1的类型(该类型不代表字符串、数组等数据类型;代表这个操作数是常量、临时变量、编译变量等)zend_ucharop2_type;//操作数2的类型zend_ucharresult_t类型;//返回值的类型};2.1.2.2操作数的表示从上面可以看出,操作数用结构体znode_op来表示,定义如下:constant、var、num等都是uint32_t类型的,这怎么表示一个操作数呢?(指针既不能代表地址,也不能代表所有数据类型);实际上,操作数、常量等大多数情况下使用的相对地址表示法表示相对于执行栈帧首地址的偏移量;另外,_znode_op结构体中有一个zval*zv字段,也可以表示一个操作数。该字段是一个指针,指向zval结构。PHP虚拟机支持的所有数据类型都用zval结构表示;typedefunion_znode_op{uint32_t常量;uint32_t变量;uint32_t数;uint32_topline_num;#ifZEND_USE_ABS_JMP_ADDRzend_op*jmp_addr;#elseuint32_tjmp_offset;#endif#ifZEND_USE_ABS_CONST_ADDRzval*zv;类型、浮点型、字符串、数组、对象等;PHP虚拟机如何存储和表示各种数据类型?2.1.2.2节指出结构体_znode_op表示一个操作数;操作数可以是一个偏移量(计算得到一个地址,即zval结构体的首地址),也可以是一个zval指针;PHP虚拟机使用zval结构来表示和存储各种数据;struct_zval_struct{zend_value值;//存储实际值union{struct{//一些标志ZEND_ENDIAN_LOHI_4(zend_uchartype,//重要;表示变量类型zend_uchartype_flags,zend_ucharconst_flags,zend_ucharreserved)/*callinfoforEX(This)*/}v;uint32_t类型信息;}u1;union{//其他有用的信息uint32_tnext;/*哈希冲突链*/uint32_tcache_slot;/*文字缓存槽*/uint32_tlineno;/*行号(对于ast节点)*/uint32_tnum_args;/*EX(This)的参数编号*/uint32_tfe_pos;/*foreach位置*/uint32_tfe_iter_idx;/*foreach迭代器索引*/uint32_taccess_flags;/*类常量访问标志*/uint32_tproperty_guard;/*singlepropertyguard*/}u2;};zval.u1.type表示数据类型,zend_types.h文件定义了以下类型:#defineIS_UNDEF0#defineIS_NULL1#defineIS_FALSE2#defineIS_TRUE3#defineIS_LONG4#defineIS_DOUBLE5#defineIS_STRING6#defineIS_ARRAY7#defineIS_OBJECT8#defineIS_RESOURCE9#defineIS_REFERENCE10…………Zend_value结构存储具体的数据内容,定义为如下:_zend_value占用16字节内存;long和double类型将直接存储在结构中;引用、字符串、数组和其他类型使用指针存储;代码中根据zval.u1.type字段判断数据类型。操作_zend_value结构体的which字段;可以看出字符串用zend_string表示,数组用zend_array表示...typedefunion_zend_value{zend_longlval;双dval;zend_refcounted*计数;zend_string*str;zend_array*arr;zend_object*obj;zend_resource*res;zend_reference*ref;zend_ast_ref*ast;zval*zv;无效*指针;zend_class_entry*ce;zend_function*func;w1;uint32_tw2;}ww;}zend_value;下图是PHP7中的字符串结构图:2.3说说指令2.1.2.1指出指令用结构体_zend_op表示;最重要的两个属性:操作函数、操作数(两个操作数和一个返回值);操作数的类型(常量、临时变量等)不同,同一条指令对应的处理函数也会不同;操作数类型在Zend/zend_compile.h文件中定义://constant#defineIS_CONST(1<<0)//临时变量,用于操作的中间结果;不能被其他指令对应的handlers重用#defineIS_TMP_VAR(1<<1)//这个变量没有在PHP代码中声明变量是返回的普通临时变量,比如$a=time(),返回值的类型ofthefunctiontime是IS_VAR,这种类型的变量可以被其他指令对应的handler重用#defineIS_VAR(1<<2)#defineIS_UNUSED(1<<3)/*Unusedvariable*///Compilevariable;即PHP中声明的变量;#defineIS_CV(1<<4)/*编译变量*/运算函数命名规则为: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,itsoperationfunctionis:ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER;函数实现是:staticZEND_OPCODE_HANDLER_RETZEND_FASTCALLZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS){USE_OPLINEzval*value;zval*variable_ptr;SAVE_OPLINE();//获取op2对应的值,即1value=EX_CONSTANT(opline->op2);//获取op1在execute_data中的位置,也是$a(execute_data类似于一个函数栈帧,后面会详细分析)variable_ptr=_get_zval_ptr_cv_undef_BP_VAR_W(execute_data,opline->op1.var);//赋值=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最后;zend_op*操作码;//类型为IS_CV的变量个数intlast_var;//类型为IS_VAR和IS_TEMP_VAR的变量个数Numberuint32_tT;//数组zend_string**vars存放IS_CV类型变量;…………//静态变量HashTable*static_variables;//常数个数;常量数组intlast_literal;zval*文字;...};注: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*结束;zend_vm_stackprev;};考虑如何设计函数执行时的帧结构:执行当前函数时,需要存储函数的编译指令,需要存储函数内部的局部变量(2.1.2.2指出操作数由结构体znode_op表示,其内部使用uint32_t来表示操作数,表示当前zval变量相对于当前函数栈帧首地址的偏移量);PHP虚拟机使用结构_zend_execute_data存储当前函数执行所需的数据;struct_zend_execute_data{//当前指令constzend_op*opline;//当前函数执行栈帧zend_execute_data*call;//函数返回数据zval*return_value;zend_function*func;zval这个;/*this+call_info+num_args*///调用当前函数的栈帧zend_execute_data*prev_execute_data;//符号表zend_array*symbol_table;#ifZEND_EX_USE_RUN_TIME_CACHEvoid**run_time_cache;#endif#ifZEND_EX_USE_LITERALS//常量数组zval*literals;#万一};当函数开始执行时,需要为函数分配相应的函数栈帧,并入栈。代码如下:staticzend_always_inlinezend_execute_data*zend_vm_stack_push_call_frame(uint32_tcall_info,zend_function*func,uint32_tnum_args,zend_class_entry*called_scope,zend_object*object){//计算当前函数栈帧所需内存空间大小uint32_tused_stack=zend_vm_calc_used_stack(num_args,func);//根据栈帧的大小分配空间并入栈zend_function*func){//_zend_execute_data大小(80字节/16字节=5)+参数个数uint32_tused_stack=ZEND_CALL_FRAME_SLOT+num_args;if(EXPECTED(ZEND_USER_CODE(func->type))){//当前使用的函数临时变量数_stack+=func->op_array.last_var+func->op_array.T-MIN(func->op_array.num_args,num_参数);}//乘以16字节returnused_stack*sizeof(zval);}//进入staticzend_always_inlinezend_execute_data*zend_vm_stack_push_call_frame_ex(uint32_tused_stack,uint32_tcall_info,zend_function*func,uint32_tnum_args,zend_class_entry*called_*scope)//上一个函数栈帧地址zend_execute_data*call=(zend_execute_data*)EG(vm_stack_top);//移动函数调用栈顶指针EG(vm_stack_top)=(zval*)((char*)call+used_stack);//初始化当前函数栈帧zend_vm_init_call_frame(call,call_info,func,num_args,called_scope,object);//返回当前函数栈帧returncall的首地址;}由上面的分析可以得到函数栈帧结构图如下:总结PHP虚拟机也是一台计算机。需要重点关注三点:指令集(包括指令处理函数)、数据存储(zval)、函数栈帧;此时虚拟机可以接受指令并执行指令代码;但是,PHP虚拟机专门用于执行PHP代码。PHP代码如何转换成PHP虚拟机可以识别的指令——编译;PHP虚拟机还提供了一个编译器,可以将PHP代码转换成一组它可以识别的指令;理论你可以自定义任何语言,只要你实现一个编译器,可以将你自己的语言转换成PHP可以识别的指令代码,就可以被PHP虚拟机执行;
