baiyan所有视频:https://segmentfault.com/a/11...复习基本概念先复习几个基本概念:opline:inzend虚拟机中,每条指令都是一条opline,每条opline由操作数、指令操作、返回值组成。Opcode:每条指令操作对应一个opcode(如ZEND_ASSIGN/ZEND_ADD等),在PHP7中,有100多种指令操作,所有的指令集都称为opcodeshandler:每条opcode指令操作对应一个handler指令处理函数,处理函数具有特定的指令操作执行逻辑。我们知道在编译阶段(zend_compile函数)之后,我们生成AST并遍历它一条条生成指令,每条指令就是一条opline。之后通过pass_two函数生成这些指令对应的handler,并将信息保存在op_array中。现在已经生成了指令和处理程序,接下来的任务就是交给zend虚拟机,加载这些指令,最后执行相应的处理程序逻辑。在PHP7中,指令由以下元素组成:struct_zend_op{constvoid*handler;//操作执行函数znode_opop1;//操作数1znode_opop2;//操作数2znode_op结果;//返回值uint32_textended_value;//扩展值uint32_tlineno;//行号zend_uchar操作码;//操作码值zend_ucharop1_type;//操作数1的类型zend_ucharop2_type;//操作数2的类型zend_ucharresult_type;//返回值类型};在PHP7中,每个操作数有5种类型可供选择,如下:#defineIS_CONST(1<<0)#defineIS_TMP_VAR(1<<1)#defineIS_VAR(1<<2)#defineIS_UNUSED(1<<3)/*未使用变量*/#defineIS_CV(1<<4)/*编译变量*/IS_CONST类型:值为1,表示常量,如$a=1或$a="helloworld中的1"helloworldIS_TMP_VAR类型:值为2,表示临时变量,如$a=”123”.time();这里拼接的临时变量“123”.time()的类型是IS_TMP_VAR,一般用于运算的中间结果IS_VAR类型:值为4,表示一个变量,但是这个变量不是普通的PHP中声明变量,返回的是临时变量,如$a=time()中的time()IS_UNUSED:值为8,表示未使用OperandIS_CV:值为16,表示遍历完带有$a这样的变量的AST,所有指令集(oplines)最终存放的地方是op_array:struct_zend_op_array{uint32_t最后;//zend_op*opcodes下面oplines数组的大小;//Oplines数组,存放所有指令intlast_var;//IS_CV类型的操作数个数uint32_tT;//IS_VAR和IS_TMP_VAR类型的操作数个数和zend_string**vars;//存放IS_CV类型操作数的数组...intlast_literal;//后面的常量数组大小zval*literals;//存放IS_CONST类型操作数的数组};op_array的存储是回顾op_array的存储情况,我们看一下gdb,用下面的测试用例:symbol_table=zend_rebuild_symbol_table();}else{execute_data->symbol_table=&EG(symbol_table);}EX(prev_execute_data)=EG(current_execute_data);i_init_code_execute_data(execute_data,op_array,return_value);zend_execute_ex(执行数据);zend_vm_stack_free_call_frame(execute_data);}观察第一行,声明了一个zend_execute_data类型的指针,这个类型很重要,存放虚拟机执行指令时的基本信息:struct_zend_execute_data{constzend_op*咨询;//当前执行的指令8Bzend_execute_data*call;//指向自身的指针8Bzval*return_value;//存储返回值8Bzend_function*func;//执行函数8BzvalThis;/*this+call_info+num_args16B*/zend_execute_data*prev_execute_data;//链表,指向前面的zend_execute_data8Bzend_array*symbol_table;//符号表8B#ifZEND_EX_USE_RUN_TIME_CACHEvoid**run_time_cache;/*缓存op_array->USLI_time_cacheZvalEXAL8B*E_#endif*literals;/*缓存op_array->literals8B*/#endif};可以看到这个zend_execute_data一共是80字节然后执行zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE|ZEND_CALL_HAS_SYMBOL_TABLE,(zend_function*)op_array,0,zend_scpeled(cal_scpeled(exe)),zend_get_this_object(EG(current_execute_data)));这个函数,我们进去看看: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);returnzend_vm_stack_push_call_frame_ex(used_stack,call_info,func,num_args,called_scope,object);}先不要看复杂的函数参数,直接看zend_vm_calc_used_stack(num_args,func);此函数调用用于计算虚拟机在执行堆栈帧上使用的空间。此时,它不应占用任何空间。我们打印used_stack:发现这里used_stack真的是0,那么进入下一个if,继续执行used_stack+=func->op_array.last_var+func->op_array.T-MIN(func->op_array.num_args,num_args);这与功能有关,我们也是没有讲,那么我们直接这个函数外层外层返回返回返回zend_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_object*object){zend_execute_data*call=(zend_execute_data*)EG(vm_stack_top);ZEND_ASSERT_VM_STACK_GLOBAL;if(UNEXPECTED(used_stack>(size_t)(((char*)EG(vm_stack_end))-(char*)call))){call=(zend_execute_data*)zend_vm_stack_extend(used_stack);ZEND_ASSERT_VM_STACK_GLOBAL;zend_vm_init_call_frame(call,call_info|ZEND_CALL_ALLOCATED,func,num_args,called_scope,object);回电;}else{EG(vm_stack_top)=(zval*)((char*)call+used_stack);zend_vm_init_call_frame(call,call_info,func,num_args,called_scope,object);回电;}}也忽略复杂的函数参数,只关注传入的used_stack=112先看第一行:将executor_globals中的vm_stack_top字段赋值给当前的zend_execute_data指向自己的指针,表示返回的是zend_execute_data的起始地址EG宏的值,查看这个值:可以看到zend_execute_data的起始地址是0x7ffff5e1c030,继续执行代码:下面的if是用来判断栈空间是否足够的?如果堆栈空间被使用过多,则需要重新分配堆栈空间。很明显我们这里没有输入这个if,说明栈空间还是足够的,那就执行下面的else。重点是:EG(vm_stack_top)=(zval*)((char*)call+used_stack);现在栈顶的位置变成了0x7ffff5e1c0a0,也就是0x7ffff5e1c030+112的结果。至于指针加法步长的操作,本质上就是addressa+stepsize*sizeof(addresstype)(如果地址类型是char*,则步长为1;如果是Int*,则步长为4),例如:int*p;p+3;如果p的地址是0x7ffff5e1c030,那么p+3的结果应该是0x7ffff5e1c030+3*sizeof(int)=0x7ffff5e1c03c我们在栈上画结构图在这time:此时,返回值call是栈顶的位置,但是栈顶指针并没有指向栈顶,而是指向了栈中部:接下来返回到最外层的zend_execute函数,然后继续执行:可以看到,符号表的内容会被赋值给execute_data中的symbol_table字段。这个符号表是一个zend_array。此时只有默认的_GET等几个预加的符号,并没有我们自己的$a:然后我们继续往下看,注意i_init_code_execute_data()函数:staticzend_always_inlinevoidi_init_code_execute_data(zend_execute_data*execute_data,zend_op_array*op_array,zval*return_value)/*{{{*/{ZEND_ASSERT(EX(func)==(zend_function*)op_array);EX(op_array->opcodes;EX(call)=NULL;EX(return_value)=return_value;zend_attach_symbol_table(execute_data);if(!op_array->run_time_cache){op_array->run_time_cache=emalloc(op_array->cache_size);>run_time_cache,0,op_array->cache_size);}EX_LOAD_RUN_TIME_CACHE(op_ar射线);EX_LOAD_LITERALS(op_array);EG(current_execute_data)=execute_data;}这里EX宏对应全局变量execute_data,EG宏对应全局变量executor_globals。需要区分重点关注zend_attach_symbol_table(execute_data)函数:HashTable*ht=execute_data->symbol_table;/*将符号表中的实际值复制到CV槽中,并在符号表中创建对CV的间接引用*///将符号表中的实际值复制到CV槽中,并在符号表中创建对CV变量的间接引用符号表if(EXPECTED(op_array->last_var)){zend_string**str=op_array->vars;zend_string**end=str+op_array->last_var;zval*var=EX_VAR_NUM(0);做{zval*zv=zend_hash_find(ht,*str);如果(zv){如果(Z_TYPE_P(zv)==IS_INDIRECT){zval*val=Z_INDIRECT_P(zv);ZVAL_COPY_VALUE(var,val);}else{ZVAL_COPY_VALUE(var,zv);}}else{ZVAL_UNDEF(var);zv=zend_hash_add_new(ht,*str,var);}ZVAL_INDIRECT(zv,var);海峡++;变量++;}while(str!=end);}}我们此时的符号表只包含_GET等默认初始化的变量,并没有包含我们自己的$a首先进入if,因为last_var=1($a),所以给str和end赋值,它们指向vars和the分别在vars后面1个offset的位置,如图:接下来遍历符号表ht,查找是否有CV变量$a,现在肯定没有,所以进入else分支,执行ZVAL_UNDEF(var)和zv=zend_hash_add_new(ht,*str,var);上面的宏EX_VAR_NUM(0)是申请一个CVslotsize的空间,但是我们这里不用,所以ZVAL_UNDEF(var)将这个slot中的zval类型设置为IS_UNDEF类型,然后加上$a符号表的zend_array通过zend_hash_add_new。那么如果下次引用$a,就会走上面的if分支,这样CV槽就派上用场了。将$a复制到CV槽中,通过间接引用就可以在符号表中找到,不需要多次添加到符号表中,节省时间和空间。Finally,movethepositionofthestrandvarpointersback,indicatingthatthistraversaliscompletedandreturnstothei_init_code_execute_datafunction.Thefollowinglinesarethecodeusedtooperatetheruntimecache.Weskipittemporarilyandreturntothezend_executemainfunction,whichwillbecallednextzend_execute()函数,在这里真正执行指令所对应的handler逻辑:赋值操作对应的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,我们看看这个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();//Getthevaluecorrespondingtoop2fromtheliteralsarray,thatis,value2value=EX_CONSTANT(opline->op2);//Getthepositionofop1inthesymboltableofexecute_data,thatis,$avariable_ptr=_get_zval_ptr_cv_undef_BP_VAR_W(execute_data,opline->op1.var);...//finallyassign1to$avalue=zend_assign_to_variable(variable_ptr,value,IS_CONST);...}Inthisway,anassignmentcommandisexecutedbythevirtualmachine,thenthereisareturn1defaultscriptreturnvalueinstruction,whichisthesame,anditwillnotbeexpandedhere,sothefinalvirtualmachineexecutionstackframeisasfollows:returntothezend_executemainfunction,andfinallycallthezend_vm_stack_free_call_frame(execute_data)function,andfinallyreleasethestackspaceoccupiedbythevirtualmachine,complete.Referencematerials[PHP7sourcecodeanalysis]PHP7sourcecoderesearchonZendvirtualmachine[PHP7sourcecodeanalysis]howtounderstandPHPvirtualmachine(1)
