深入剖析php执行原理(2):函数的编译,深入剖析php本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。
想讲清楚在zend vm中,函数如何被正确的编译成op指令、如何发生参数传递、如何模拟调用栈、如何切换作用域等等,的确是一个很大范畴的话题。但为了弄明白php的原理,必须要攻克它。
对函数的研究,大致可以分成两块。第一块是函数体的编译,主要涉及到如何将函数转化成zend_op指令。第二块是研究函数的调用,涉及到函数调用语句的编译,以及函数如何被执行等topic。这里先来看看函数如何被编译,我们下一篇再讲函数的调用。
函数的编译对函数进行编译,最终目的是为了生成一份对应的op指令集,除了op指令集,编译函数还会产生其他一些相关的数据,比如说函数名称、参数列表信息、compiled variables,甚至函数所在文件,起始行数等等。这些信息作为编译的产出,都需要保存起来。保存这些产出的数据结构,正是上一节中所描述的zend_op_array。下文会以op_array作为简称。
下面列出了一个简单的例子:
u.constant.value.str.val;int name_len = function_name->u.constant.value.str.len;int function_begin_line = function_token->u.opline_num;zend_uint fn_flags;char *lcname;zend_bool orig_interactive;alloca_flag(use_heap)if (is_method) { ...} else { fn_flags = 0;}// 对函数来说,fn_flags没用,对方法来说,fn_flags指定了方法的修饰符if ((fn_flags & zend_acc_static) && (fn_flags & zend_acc_abstract) && !(cg(active_class_entry)->ce_flags & zend_acc_interface)) { zend_error(e_strict, static function %s%s%s() should not be abstract, is_method ? cg(active_class_entry)->name : , is_method ? :: : , z_strval(function_name->u.constant));}
这段代码一开始就印证了我们先前的说法,每个函数都有一份自己的op_array。所以会在开头先声明一个op_array变量。
// 第一个znode参数的妙处,它记录了当前的cg(active_op_array)function_token->u.op_array = cg(active_op_array);lcname = zend_str_tolower_dup(name, name_len);// 对op_array进行初始化,强制op_array.fn_flags会被初始化为0orig_interactive = cg(interactive);cg(interactive) = 0;init_op_array(&op_array, zend_user_function, initial_op_array_size tsrmls_cc);cg(interactive) = orig_interactive;// 对op_array的一些设置op_array.function_name = name;op_array.return_reference = return_reference;op_array.fn_flags |= fn_flags;op_array.pass_rest_by_reference = 0;op_array.scope = is_method ? cg(active_class_entry):null;op_array.prototype = null;op_array.line_start = zend_get_compiled_lineno(tsrmls_c);
function_token便是对function字面进行词法分析而生成的znode。这段代码一开始,就让它保存当前的cg(active_op_array),即函数体之外的op_array。保存好cg(active_op_array)之后,便会开始对函数自己的op_array进行初始化。
op_array.fn_flags是个多功能字段,还记得上一篇中提到的交互式么,如果php以交互式打开,则op_array.fn_flags会被初始化为zend_acc_interactive,否则会被初始化为0。这里在init_op_array之前设置cg(interactive) = 0,便是确保op_array.fn_flags初始化为0。随后会进一步执行op_array.fn_flags |= fn_flags,如果是在方法中,则op_array.fn_flags含义为static、abstract、final等修饰符,对函数来讲,op_array.fn_flags依然是0。
zend_op *opline = get_next_op(cg(active_op_array) tsrmls_cc);// 如果处于命名空间,则函数名还需要加上命名空间if (cg(current_namespace)) { /* prefix function name with current namespace name */ znode tmp; tmp.u.constant = *cg(current_namespace); zval_copy_ctor(&tmp.u.constant); zend_do_build_namespace_name(&tmp, &tmp, function_name tsrmls_cc); op_array.function_name = z_strval(tmp.u.constant); efree(lcname); name_len = z_strlen(tmp.u.constant); lcname = zend_str_tolower_dup(z_strval(tmp.u.constant), name_len);}// 设置oplineopline->opcode = zend_declare_function;// 第一个操作数opline->op1.op_type = is_const;build_runtime_defined_function_key(&opline->op1.u.constant, lcname, name_len tsrmls_cc);// 第二个操作数opline->op2.op_type = is_const;opline->op2.u.constant.type = is_string;opline->op2.u.constant.value.str.val = lcname;opline->op2.u.constant.value.str.len = name_len;z_set_refcount(opline->op2.u.constant, 1);opline->extended_value = zend_declare_function;// 切换cg(active_op_array)成函数自己的op_arrayzend_hash_update(cg(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, sizeof(zend_op_array), (void **) &cg(active_op_array));
上面这段代码很关键。有几点要说明的:
1,如果函数是处于命名空间中,则其名称会被扩展成命名空间\函数名。比如:
opcode = zend_ext_nop; opline->lineno = function_begin_line; set_unused(opline->op1); set_unused(opline->op2);}// 控制switch和foreach内声明的函数{ /* push a seperator to the switch and foreach stacks */ zend_switch_entry switch_entry; switch_entry.cond.op_type = is_unused; switch_entry.default_case = 0; switch_entry.control_var = 0; zend_stack_push(&cg(switch_cond_stack), (void *) &switch_entry, sizeof(switch_entry)); { /* foreach stack separator */ zend_op dummy_opline; dummy_opline.result.op_type = is_unused; dummy_opline.op1.op_type = is_unused; zend_stack_push(&, (void *) &dummy_opline, sizeof(zend_op)); }}// 保存函数的注释语句if (cg(doc_comment)) { cg(active_op_array)->doc_comment = cg(doc_comment); cg(active_op_array)->doc_comment_len = cg(doc_comment_len); cg(doc_comment) = null; cg(doc_comment_len) = 0;}// 作用和上面switch,foreach是一样的,函数体内的语句并不属于函数体外的labelzend_stack_push(&cg(labels_stack), (void *) &cg(labels), sizeof(hashtable*));cg(labels) = null;
可能初学者会对cg(switch_cond_stack),cg(foreach_copy_stack),cg(labels_stack)等字段有疑惑。其实也很好理解。以cg(labels_stack)为例,由于进入函数体内之后,op_array发生了切换,外层的cg(active_op_array)被保存到function znode的u.op_array中(如果记不清楚了回头看上文:-))。因此函数外层已经被parse出的一些label也需要被保存下来,用的正是cg(labels_stack)来保存。当函数体完成编译之后,zend vm可以从cg(labels_stack)中恢复出原先的label。举例来说,
vars数组中。虽然根据变量名称去hashtable查询,效率并不低。但显然根据索引去op_array->vars数组中获取变量,会更加高效。
void fetch_simple_variable_ex(znode *result, znode *varname, int bp, zend_uchar op tsrmls_dc) /* {{{ */{ zend_op opline; ... if (varname->op_type == is_const) { if (z_type(varname->u.constant) != is_string) { convert_to_string(&varname->u.constant); } if (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len tsrmls_cc) && !(varname->u.constant.value.str.len == (sizeof(this)-1) && !memcmp(varname->u.constant.value.str.val, this, sizeof(this))) && (cg(active_op_array)->last == 0 || cg(active_op_array)->opcodes[cg(active_op_array)->last-1].opcode != zend_begin_silence)) { // 节点的类型为is_cv,表明是compiled variables result->op_type = is_cv; // 用u.var来记录compiled variables在cg(active_op_array)->vars中的索引 result->u.var = lookup_cv(cg(active_op_array), varname->u.constant.value.str.val, varname->u.constant.value.str.len); result->u.ea.type = 0; varname->u.constant.value.str.val = cg(active_op_array)->vars[result->u.var].name; return; } } ...}
这里不做详细的分析了。当fetch_simple_variable获取索引之后,znode中就不必再保存变量的名称,取而代之的是变量在vars数组中的索引,即znode->u.var,其类型为int。fetch_simple_variable完成,会进入zend_do_receive_arg。
3.2 zend_do_receive_argzend_do_receive_arg目的是生成一条zend op指令,可以称作recv。
一般而言,除非函数不存在参数,否则recv是函数的第一条指令(这里表述不准,有extend info时也不是第一条)。该指令的opcode可能为zend_recv或者zend_recv_init,取决于是否有默认值。如果参数没有默认值,指令等于zend_recv,有默认值,则为zend_recv_init。zend_do_receive_arg的第二个参数,就是上面提到的compiled variables节点。
分析下zend_do_receive_arg的源码,也是分几段来看:
zend_op *opline;zend_arg_info *cur_arg_info;// class_type主要用于限制函数参数的类型if (class_type->op_type == is_const && z_type(class_type->u.constant) == is_string && z_strlen(class_type->u.constant) == 0) { /* usage of namespace as class name not in namespace */ zval_dtor(&class_type->u.constant); zend_error(e_compile_error, cannot use 'namespace' as a class name); return;}// 对静态方法来说,参数不能为thisif (var->op_type == is_cv && var->u.var == cg(active_op_array)->this_var && (cg(active_op_array)->fn_flags & zend_acc_static) == 0) { zend_error(e_compile_error, cannot re-assign $this);} else if (var->op_type == is_var && cg(active_op_array)->scope && ((cg(active_op_array)->fn_flags & zend_acc_static) == 0) && (z_type(varname->u.constant) == is_string) && (z_strlen(varname->u.constant) == sizeof(this)-1) && (memcmp(z_strval(varname->u.constant), this, sizeof(this)) == 0)) { zend_error(e_compile_error, cannot re-assign $this);}// cg(active_op_array)此时已经是函数体的op_array了,这里拿一条指令opline = get_next_op(cg(active_op_array) tsrmls_cc);cg(active_op_array)->num_args++;opline->opcode = op;opline->result = *var;// op1节点表明是第几个参数opline->op1 = *offset;// op2节点可能为初始值,也可能为unusedif (op == zend_recv_init) { opline->op2 = *initialization;} else { cg(active_op_array)->required_num_args = cg(active_op_array)->num_args; set_unused(opline->op2);}
上面这段代码,首先通过get_next_op(cg(active_op_array) tsrmls_cc)一句获取了opline,opline是未被使用的一条zend_op指令。紧接着,会对opline的各个字段进行设置。opline->op1表明这是第几个参数,opline->op2可能为初始值,也可能被设置为unused。
如果一个参数有默认值,那么在调用函数时,其实是可以不用传递该参数的。所以,required_num_args不会将这类非必须的参数算进去的。可以看到,在op == zend_recv_init这段逻辑分支中,并没有处理required_num_args。
继续来看:
// 这里采用erealloc进行分配,因为期望最终会形成一个参数信息的数组cg(active_op_array)->arg_info = erealloc(cg(active_op_array)->arg_info, sizeof(zend_arg_info)*(cg(active_op_array)->num_args));// 设置当前的zend_arg_infocur_arg_info = &cg(active_op_array)->arg_info[cg(active_op_array)->num_args-1];cur_arg_info->name = estrndup(varname->u.constant.value.str.val, varname->u.constant.value.str.len);cur_arg_info->name_len = varname->u.constant.value.str.len;cur_arg_info->array_type_hint = 0;cur_arg_info->allow_null = 1;cur_arg_info->pass_by_reference = pass_by_reference;cur_arg_info->class_name = null;cur_arg_info->class_name_len = 0;// 如果需要对参数做类型限定if (class_type->op_type != is_unused) { cur_arg_info->allow_null = 0; // 限定为类 if (class_type->u.constant.type == is_string) { if (zend_fetch_class_default == zend_get_class_fetch_type(z_strval(class_type->u.constant), z_strlen(class_type->u.constant))) { zend_resolve_class_name(class_type, &opline->extended_value, 1 tsrmls_cc); } cur_arg_info->class_name = class_type->u.constant.value.str.val; cur_arg_info->class_name_len = class_type->u.constant.value.str.len; // 如果限定为类,则参数的默认值只能为null if (op == zend_recv_init) { if (z_type(initialization->u.constant) == is_null || (z_type(initialization->u.constant) == is_constant && !strcasecmp(z_strval(initialization->u.constant), null))) { cur_arg_info->allow_null = 1; } else { zend_error(e_compile_error, default value for parameters with a class type hint can only be null); } } } // 限定为数组 else { // 将array_type_hint设置为1 cur_arg_info->array_type_hint = 1; cur_arg_info->class_name = null; cur_arg_info->class_name_len = 0; // 如果限定为数组,则参数的默认值只能为数组或null if (op == zend_recv_init) { if (z_type(initialization->u.constant) == is_null || (z_type(initialization->u.constant) == is_constant && !strcasecmp(z_strval(initialization->u.constant), null))) { cur_arg_info->allow_null = 1; } else if (z_type(initialization->u.constant) != is_array && z_type(initialization->u.constant) != is_constant_array) { zend_error(e_compile_error, default value for parameters with array type hint can only be an array or null); } } }}opline->result.u.ea.type |= ext_type_unused;
这部分代码写的很清晰。注意,对于限定为数组的情况,class_type的op_type会被设置为is_const,而u.constant.type会被设置为is_null:
optional_class_type: /* empty */ { $$.op_type = is_unused; } | fully_qualified_class_name { $$ = $1; } | t_array { $$.op_type = is_const; z_type($$.u.constant)=is_null;}
因此,zend_do_receive_arg中区分限定为类还是数组,是利用class_type->u.constant.type == is_string来判断的。如果类型限定为数组,则cur_arg_info->array_type_hint会被设置为1。
还有另一个地方需要了解,zend_resolve_class_name函数会修正类名。举例来说:
function_name); zend_str_tolower_copy(lcname, cg(active_op_array)->function_name, min(name_len, sizeof(lcname)-1)); lcname[sizeof(lcname)-1] = '\0'; /* zend_str_tolower_copy won't necessarily set the zero byte */ // 检查__autoload函数的参数是否合法 if (name_len == sizeof(zend_autoload_func_name) - 1 && !memcmp(lcname, zend_autoload_func_name, sizeof(zend_autoload_func_name)) && cg(active_op_array)->num_args != 1) { zend_error(e_compile_error, %s() must take exactly 1 argument, zend_autoload_func_name); } }cg(active_op_array)->line_end = zend_get_compiled_lineno(tsrmls_c);// 很关键!将cg(active_op_array)还原成函数外层的op_arraycg(active_op_array) = function_token->u.op_array;/* pop the switch and foreach seperators */zend_stack_del_top(&cg(switch_cond_stack));zend_stack_del_top(&cg(foreach_copy_stack));
有3处值得注意:
1,zend_do_end_function_declaration中会对cg(active_op_array)进行还原。用的正是function_token->u.op_array。一旦zend_do_end_function_declaration完成,函数的整个编译过程就已经结束了。zend vm会继续看接下来函数之外的代码,所以需要将cg(active_op_array)切换成原先的。
2,zend_do_return负责在函数最后添加上一条return指令,因为我们传进去的是null,所以这条return指令的操作数被强制设置为unused。注意,不管函数本身是否有return语句,最后这条return指令是必然存在的。假如函数有return语句,return语句也会产生一条return指令,所以会导致可能出现多条return指令。举例来说:
function foo()
{ return true;}
编译出来的op指令最后两条如下:
return true return null
我们可以很明显在最后看到两条return。一条是通过return true编译出来的。另一条,就是在zend_do_end_function_declaration阶段,强制插入的return。
3,我们刚才讲解的所有步骤中,都只是设置了每条指令的opcode,而并没有设置这条指令具体的handle函数。pass_two会负责遍历每条zend_op指令,根据opcode,以及操作数op1和op2,去查找并且设置对应的handle函数。这项工作,是通过zend_vm_set_opcode_handler(opline)宏来完成的。
#define zend_vm_set_opcode_handler(opline) zend_vm_set_opcode_handler(opline)
zend_vm_set_opcode_handler的实现很简单:
void zend_init_opcodes_handlers(void){ // 超大的数组,里面存放了所有的handler static const opcode_handler_t labels[] = { zend_nop_spec_handler, zend_nop_spec_handler, zend_nop_spec_handler, zend_nop_spec_handler, zend_nop_spec_handler, zend_nop_spec_handler, ... }; zend_opcode_handlers = (opcode_handler_t*)labels;}static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op){ static const int zend_vm_decode[] = { _unused_code, /* 0 */ _const_code, /* 1 = is_const */ _tmp_code, /* 2 = is_tmp_var */ _unused_code, /* 3 */ _var_code, /* 4 = is_var */ _unused_code, /* 5 */ _unused_code, /* 6 */ _unused_code, /* 7 */ _unused_code, /* 8 = is_unused */ _unused_code, /* 9 */ _unused_code, /* 10 */ _unused_code, /* 11 */ _unused_code, /* 12 */ _unused_code, /* 13 */ _unused_code, /* 14 */ _unused_code, /* 15 */ _cv_code /* 16 = is_cv */ }; // 去handler数组里找到对应的处理函数 return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];}zend_api void zend_vm_set_opcode_handler(zend_op* op){ // 给zend op设置对应的handler函数 op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);}
所有的opcode都定义在zend_vm_opcodes.h里,从php5.3-php5.6,大概从150增长到170个opcode。上面可以看到通过opcode查找handler的准确算法:
zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]
不过zend_opcode_handlers数组太大了...找起来很麻烦。
下面回到文章开始的那段php代码,我们将函数foo进行编译,最终得到的指令如下:
可以看出,因为foo指接受一个参数,所以这里只有一条recv指令。
print语句的参数为!0,!0是一个compiled variables,其实就是参数中的arg1。0代表着索引,回忆一下,函数的op_array有一个数组专门用于保存compiled variables,0表明arg1位于该数组的开端。
print语句有返回值,所以会存在一个临时变量保存其返回值,即~0。由于我们在函数中并未使用~0,所以随即便会有一条free指令对其进行释放。
在函数的最后,是一条return指令。
6、绑定函数编译完成之后,还需要进行的一步是绑定。zend vm通过zend_do_early_binding来实现绑定。这个名字容易让人产生疑惑,其实只有在涉及到类和方法的时候,才会有早期绑定,与之相对的是延迟绑定,或者叫后期绑定。纯粹函数谈不上这种概念,不过zend_do_early_binding是多功能的,并非仅仅为绑定方法而实现。
来看下zend_do_early_binding:
// 拿到的是最近一条zend op,对于函数来说,就是zend_declare_functionzend_op *opline = &cg(active_op_array)->opcodes[cg(active_op_array)->last-1];hashtable *table;while (opline->opcode == zend_ticks && opline > cg(active_op_array)->opcodes) { opline--;}switch (opline->opcode) { case zend_declare_function: // 真正绑定函数 if (do_bind_function(opline, cg(function_table), 1) == failure) { return; } table = cg(function_table); break; case zend_declare_class: ... case zend_declare_inherited_class: ...}// op1中保存的是函数的key,这里其从将cg(function_table)中删除zend_hash_del(table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);zval_dtor(&opline->op1.u.constant);zval_dtor(&opline->op2.u.constant);// opline置为nopmake_nop(opline);
这个函数实现也很简单,主要就是调用了do_bind_function。
zend_api int do_bind_function(zend_op *opline, hashtable *function_table, zend_bool compile_time) /* {{{ */{ zend_function *function; // 找出函数 zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (void *) &function); // 以函数名称作为key,重新加入function_table if (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, function, sizeof(zend_function), null)==failure) { int error_level = compile_time ? e_compile_error : e_error; zend_function *old_function; // 加入失败,可能发生重复定义了 if (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, (void *) &old_function)==success && old_function->type == zend_user_function && old_function->op_array.last > 0) { zend_error(error_level, cannot redeclare %s() (previously declared in %s:%d), function->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[0].lineno); } else { zend_error(error_level, cannot redeclare %s(), function->common.function_name); } return failure; } else { (*function->op_array.refcount)++; function->op_array.static_variables = null; /* null out the unbound function */ return success; }}
在进入do_bind_function之前,其实cg(function_table)中已经有了函数的op_array。不过用的键并非函数名,而是build_runtime_defined_function_key生成的“key”,这点在前面“开始编译”一节中有过介绍。do_bind_function所做的事情,正是利用这个“key”,将函数查找出来,并且以真正的函数名为键,重新插入到cg(function_table)中。
因此当do_bind_function完成时,function_table中有2个键可以查询到该函数。一个是“key”为索引的,另一个是以函数名为索引的。在zend_do_early_binding的最后,会通过zend_hash_del来删除“key”,从而保证function_table中,该函数只能够以函数名为键值查询到。
7、总结这篇其实主要是为了弄清楚,函数如何被编译成op_array。一些关键的步骤如下图:
至于函数的调用,又是另外一个话题了。
http://www.bkjia.com/phpjc/1122764.htmlwww.bkjia.comtruehttp://www.bkjia.com/phpjc/1122764.htmltecharticle深入剖析php执行原理(2):函数的编译,深入剖析php 本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。 想...