内容简介:baiyan全部视频:原视频地址:
baiyan
全部视频: https://segmentfault.com/a/11...
原视频地址: http://replay.xesv5.com/ll/24...
复习
基本概念
-
首先复习几个基本概念:
opline:在zend虚拟机中,每条指令都是一个 opline ,每个opline由操作数、指令操作、返回值组成
opcode:每个指令操作都对应一个 opcode (如ZEND_ASSIGN/ZEND_ADD等等),在 PHP 7中,有100多种指令操作,所有的指令集被称作opcodes
handler:每个opcode指令操作都对应一个 handler 指令处理函数,处理函数中有具体的指令操作执行逻辑
- 我们知道,在经过编译阶段(zend_compile函数)中,我们生成AST并对其遍历,生成一条条指令,每一条指令都是一个opline。之后通过pass_two函数生成了这些指令所对应的handler,这些信息均存在op_array中。既然指令和handler已经生成完毕,接下来的任务就是要交给zend虚拟机,加载这些指令,并最终执行对应的handler逻辑。
- 指令在PHP7中,由以下元素构成:
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; //opcode值 zend_uchar op1_type; //操作数1的类型 zend_uchar op2_type; //操作数2的类型 zend_uchar result_type; //返回值的类型 };
- 在PHP7中,每个操作数有5种类型可选,如下:
#define IS_CONST (1<<0) #define IS_TMP_VAR (1<<1) #define IS_VAR (1<<2) #define IS_UNUSED (1<<3) /* Unused variable */ #define IS_CV (1<<4) /* Compiled variable */
IS_CONST类型:值为1,表示常量,如$a = 1中的1或者$a = "hello world"中的hello world
IS_TMP_VAR类型:值为2,表示临时变量,如$a=”123”.time(); 这里拼接的临时变量”123”.time()的类型就是IS_TMP_VAR,一般用于操作的中间结果
IS_VAR类型:值为4,表示变量,但是这个变量并不是PHP中常见的声明变量,而是返回的临时变量,如$a = time()中的time()
IS_UNUSED:值为8,表示没有使用的操作数
IS_CV:值为16,表示形如$a这样的变量
- 对AST进行遍历之后,最终存放所有指令集(oplines)的地方为op_array:
struct _zend_op_array { uint32_t last; //下面oplines数组大小 zend_op *opcodes; //oplines数组,存放所有指令 int last_var;//操作数类型为IS_CV的个数 uint32_t T;//操作数类型为IS_VAR和IS_TMP_VAR的个数之和 zend_string **vars;//存放IS_CV类型操作数的数组 ... int last_literal;//下面常量数组大小 zval *literals;//存放IS_CONST类型操作数的数组 };
op_array的存储情况
- 为了复习op_array的存储情况,我们具体gdb一下,使用下面的测试用例:
<?php $a = 2;
- 根据以上测试用例,在zend_execute处打一个断点,这里完成了对AST的遍历并生成了最终的op_array,已经进入到虚拟机执行指令的入口。首先我们先观察传入的参数op_array,它是经过AST遍历之后生成的最终的op_array:
- last = 2;表示一共有两个opcodes:一个是赋值ASSIGN,另一个是脚本为我们自动生成的返回语句return 1,opcodes是一个数组,每个数组单元具体存储了每条指令的信息(操作数、返回值等等),我们打印一下数组的内容:
- last_var = 1;表示有一个CV类型的变量,这里就是$a
- T = 1;表示IS_TMP_VAR和IS_VAR变量类型的数量之和,而我们脚本中并没有这样的变量,它是在存储中间的返回值的时候,这个返回值类型就是一个IS_VAR类型,所以T的值一开始就为1
- vars是一个二级指针,可以理解为外层的一级指针首先指向一个数组,这个数组里每个存储单元都是一个zend_string*类型的指针,而每个指针都指向了一个zend_string结构体,我们打印数组第一个单元的值,发现其指向的zend_string值为a:
- last_literal = 2;表示脚本中一共有2个常量,一个是我们自己复制的值2,另一个是脚本为我们自动生成的返回语句return 1中的值1:
- literals是一个zend_array,里面每一个单元都是一个zval,存储这些常量的实际的值,我们可以看到,其值为2和1,与上面的描述相符:
- 我们可以画出最终的op_array存储结构图:
- 这样一来,我们就可以清晰地看出指令在op_array中是如何存储的。那么接下来,我们需要将其加载到虚拟机的执行栈桢上,来最终执行这些指令。
在虚拟机上执行指令
- 下面让我们真正执行op_array中的指令,执行指令的入口为zend_execute函数,传入参数为op_array以及一个zval指针:
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) { zend_execute_data *execute_data; if (EG(exception) != NULL) { return; } execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE, (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data))); if (EG(current_execute_data)) { execute_data->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(execute_data); zend_vm_stack_free_call_frame(execute_data); }
- 观察第一行,声明了一个zend_execute_data类型的指针,这个类型非常重要,存储了虚拟机执行指令时的基本信息:
struct _zend_execute_data { const zend_op *opline; //当前执行的指令 8B zend_execute_data *call; //指向自己的指针 8B zval *return_value; //存储返回值 8B zend_function *func; //执行的函数 8B zval This; /* this + call_info + num_args 16B */ zend_execute_data *prev_execute_data; //链表,指向前一个zend_execute_data 8B zend_array *symbol_table; //符号表 8B #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; /* cache op_array->run_time_cache 8B*/ #endif #if ZEND_EX_USE_LITERALS zval *literals; /* cache op_array->literals 8B */ #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_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));这个函数,我们s进去看下:
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); }
- 先不看复杂的函数参数,直接看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);这个与函数相关,我们还没有讲,那么我们直接看这个函数外层返回的used_stack值,为112B:
- 那么继续往下执行zend_vm_stack_push_call_frame_ex(used_stack, call_info,func, num_args, called_scope, object):
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); 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); return call; } else { 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; } }
- 同样忽略复杂的函数参数,只关注传入的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的结果。至于指针加法步长的运算,本质上就是 地址a + 步长 * sizeof(地址类型) (地址类型如果是char *,步长就是1;如果是Int *,步长就是4),举例子:
int *p; p+3;
- 假如p的地址是0x7ffff5e1c030,那么p+3的结果就应该是0x7ffff5e1c030 + 3 * sizeof(int) = 0x7ffff5e1c03c
- 我们画出此时栈上的结构图:
- 此时这个返回值call就是栈顶的位置,但是top指针并不指向栈顶,而是指向栈的中间:
- 接下来回到最外层的zend_execute函数,继续往下执行:
- 可以看到,接下来将符号表中的内容赋值给了execute_data中的symbol_table字段,这个符号表是一个zend_array,此时还只有几个默认的_GET这几个预先添加的符号,并没有我们自己的$a:
- 那么我们继续往下走,关注i_init_code_execute_data()函数:
static zend_always_inline void i_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(opline) = 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); memset(op_array->run_time_cache, 0, op_array->cache_size); } EX_LOAD_RUN_TIME_CACHE(op_array); EX_LOAD_LITERALS(op_array); EG(current_execute_data) = execute_data; }
- 这里的EX宏对应全局变量execute_data,EG宏对应全局变量executor_globals,要区分开
- 重点关注zend_attach_symbol_table(execute_data)函数:
ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) /* {{{ */ { zend_op_array *op_array = &execute_data->func->op_array; HashTable *ht = execute_data->symbol_table; /* copy real values from symbol table into CV slots and create INDIRECT references to CV in symbol table */ // 从符号表中拷贝真实的值到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); do { zval *zv = zend_hash_find(ht, *str); if (zv) { if (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); str++; var++; } while (str != end); } }
- 我们此时的符号表只包含_GET这类默认初始化的变量,并不包含我们自己的$a。首先进入if,因为last_var = 1($a),所以将str和end赋值,他们分别指向vars和vars后面1偏移量的位置,如图:
- 接下来在符号表ht中遍历,查找是否有$a这个CV型变量,现在肯定是没有的,所以进入else分支,执行ZVAL_UNDEF(var)与zv = zend_hash_add_new(ht, *str, var);
- 上面 EX_VAR_NUM(0)这个宏是一个申请一个CV槽大小的空间,但是在这里我们没有使用,所以ZVAL_UNDEF(var)将这个槽中的zval类型置为IS_UNDEF类型,然后通过zend_hash_add_new将$a加入到符号表这个zend_array中。那么如果下一次再引用$a的时候,就会走上面的if分支,这样CV槽就有了用武之地。把$a拷贝到CV槽中,那么在符号表中通过间接引用找到它即可,就不用多次将其加入到符号表中,节省时间与空间。最后将str与var指针的位置往后挪,说明本次遍历完成
- 回到i_init_code_execute_data函数,下面几行是用来操作运行时缓存的代码,我们暂时跳过,回到zend_execute主函数,接下来会调用zend_execute()函数,在这里真正执行指令所对应的handler逻辑:
- 赋值操作对应的是ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER,我们看看这个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(); //从literals数组中获取op2对应的值,也就是值2 value = EX_CONSTANT(opline->op2); //在execute_data的符号表中获取op1的位置,也就是$a variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var); ... //最终将1赋值给$a value = zend_assign_to_variable(variable_ptr, value, IS_CONST); ... }
- 这样,一个赋值指令就被虚拟机执行完毕,那么还有一个return 1默认的脚本返回值的指令,也是同理,这里不再展开,那么最终的虚拟机执行栈桢的情况如下:
- 回到zend_execute主函数,最后调用了zend_vm_stack_free_call_frame(execute_data)函数,最终释放虚拟机占用的栈空间,完毕。
参考资料
以上所述就是小编给大家介绍的《【PHP源码学习】2019-03-28 Zend虚拟机》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Faker 虚拟数据填充和源码解析
- Vue源码解析:虚拟dom比较原理
- Vue源码探究-虚拟DOM的渲染
- 解密虚拟 DOM——snabbdom 核心源码解读
- 从 Vue 源码角度来理解虚拟 DOM
- 根据调试工具看Vue源码之虚拟dom(二)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Linux/Unix设计思想
甘卡兹 / 漆犇 / 人民邮电出版社 / 2012-3-28 / 39.00元
《Linux\Unix设计思想/图灵程序设计丛书》内容简介:将Linux的开发方式与Unix的原理有效地结合起来,总结出Linux与Unix软件开发中的设计原则。《Linux\Unix设计思想/图灵程序设计丛书》前8章分别介绍了Linux与Unix中9条基本的哲学准则和10条次要准则。第9章和第10章将Unix系统的设计思想与其他系统的设计思想进行了对比。最后介绍了Unix哲学准则在其他领域中的应......一起来看看 《Linux/Unix设计思想》 这本书的介绍吧!