您好,欢迎访问一九零五行业门户网

深入剖析php执行原理(4):函数的调用,深入剖析php_PHP教程

深入剖析php执行原理(4):函数的调用,深入剖析php本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的。
我们前面的章节弄明白了函数体会被编译生成哪些zend_op指令,本章会研究函数调用语句会生成哪些zend_op指,等后面的章节再根据这些op指令,来剖析php运行时的细节。
源码依然取自php5.3.29。
函数调用回顾之前用的php代码示例:
zend_do_pass_param-->zend_do_end_function_call
开始 解析参数 结束
和编译function语句块时的几步(zend_do_begin_function_declaration->zend_do_receive_arg->zend_do_end_function_declaration等)顺序上比较类似。
上面提到语法树我们仅仅画了一部分,准确讲,没有将namespace以及function_call_parameter_list以下的推导过程进一步画出来。原因一是namespace的推导比较简单。第二,由于function_call_parameter_list-->variable这步会回到variable上,而variable经过若干步一直到产生变量$bar的推导比较复杂,也不是本文的重点,所以这里就不一进步探究了。
2、开始编译看下function_call的推导式,一开始,zend vm会执行zend_do_begin_function_call做一些函数调用的准备。
2.1、 zend_do_begin_function_call代码注解如下:
zend_function *function;char *lcname;char *is_compound = memchr(z_strval(function_name->u.constant), '\\', z_strlen(function_name->u.constant));// 将函数名进行修正,例如带上命名空间作为前缀等zend_resolve_non_class_name(function_name, check_namespace tsrmls_cc);// 能进入该分支,说明在一个命名空间下以shortname调用函数,会生成一条do_fcall_by_name指令if (check_namespace && cg(current_namespace) && !is_compound) { /* we assume we call function from the current namespace if it is not prefixed. */ /* in run-time php will check for function with full name and internal function with short name */ zend_do_begin_dynamic_function_call(function_name, 1 tsrmls_cc); return 1;} // 转成小写,因为cg(function_table)中的函数名都是小写lcname = zend_str_tolower_dup(function_name->u.constant.value.str.val, function_name->u.constant.value.str.len);// 如果function_table中找不到该函数,则也尝试生成do_fcall_by_name指令if ((zend_hash_find(cg(function_table), lcname, function_name->u.constant.value.str.len+1, (void **) &function) == failure) || ((cg(compiler_options) & zend_compile_ignore_internal_functions) && (function->type == zend_internal_function))) { zend_do_begin_dynamic_function_call(function_name, 0 tsrmls_cc); efree(lcname); return 1; /* dynamic */} efree(function_name->u.constant.value.str.val);function_name->u.constant.value.str.val = lcname;// 压入cg(function_call_stack)zend_stack_push(&cg(function_call_stack), (void *) &function, sizeof(zend_function *));zend_do_extended_fcall_begin(tsrmls_c);return 0;
有几点需要理解的:
1,zend_resolve_non_class_name。由于php支持命名空间、也支持别名/导入等特性,因此首先要做的是将函数名称进行修正,否则在cg(function_table)中找不到。例如,函数处于一个命名空间中,则可能需要将函数名添加上命名空间作为前缀,最终形成完整的函数名,也就是我们前文提到的以一种类似“全路径”的fullname作为函数名。再例如,函数名只是一个设置的别名,它实际指向了另一个命名空间中的某个函数,则需要将其改写成真正被调用函数的名称。这些工作,均由zend_resolve_non_class_name完成。命名空间添加了不少复杂度,下面是一些简单的例子:
foo($bar); // zend_resolve_non_class_name会将foo处理成myproject\foonamespace\foo($bar); // 在进入zend_do_begin_function_call之前,函数名已经被扩展成\myproject\foo,再经过zend_resolve_non_class_name,将\myproject\foo处理成myproject\foo\myproject\foo($bar); // zend_resolve_non_class_name会将\myproject\foo处理成myproject\foo
总之,zend_resolve_non_class_name是力图生成一个最精确、最完整的函数名。
2,cg(current_namespace)存储了当前的命名空间。check_namespace和!is_compound一起说明被调用函数在当前命名空间下的,并且以shortname名称被调用。所谓shortname,是和上述的fullname相对,shorname的函数名,不存在\。
就像上面的例子中,我们在myproject命名空间下,以foo为函数名来调用。这种情况下,check_namespace=1,is_compound = null,cg(current_namespace) = myproject。因此,会走到zend_do_begin_dynamic_function_call里进一步处理。zend_do_begin_dynamic_function_call我们下面再具体描述。
注意上述例子,我们以sub\foo来调用函数。zend_resolve_non_class_name会将函数名处理成myproject\sub\foo。不过is_compound是在zend_resolve_non_class_name之前算的,由于sub\foo存在\,所以is_compound为\foo,!is_compound是false,因而不能进入zend_do_begin_dynamic_function_call。
3,同样,如果cg(function_table)中找不到函数,也会进入zend_do_begin_dynamic_function_call进一步处理。为什么在函数表中找不到函数,因为php允许我们先调用,再去定义函数。例如:
4,在zend_do_begin_function_call的最后,我们将函数压入cg(function_call_stack)。这是一个栈,因为在后续对传参的编译,我们仍然需要用到函数,所以这里将其压亚入栈中,方便后面获取使用。之所以用栈,是因为调用函数传递的参数,可能是另一次函数调用。为了确保参数总是能找到对应的函数,所以用栈。
opcode = zend_init_ns_fcall_by_name; opline->op2 = *function_name; opline->extended_value = 0; opline->op1.op_type = is_const; z_type(opline->op1.u.constant) = is_string; z_strval(opline->op1.u.constant) = zend_str_tolower_dup(z_strval(opline->op2.u.constant), z_strlen(opline->op2.u.constant)); z_strlen(opline->op1.u.constant) = z_strlen(opline->op2.u.constant); opline->extended_value = zend_hash_func(z_strval(opline->op1.u.constant), z_strlen(opline->op1.u.constant) + 1); // 再拿一条zend_op,指令为zend_op_data slash = zend_memrchr(z_strval(opline->op1.u.constant), '\\', z_strlen(opline->op1.u.constant)); prefix_len = slash-z_strval(opline->op1.u.constant)+1; name_len = z_strlen(opline->op1.u.constant)-prefix_len; opline2 = get_next_op(cg(active_op_array) tsrmls_cc); opline2->opcode = zend_op_data; opline2->op1.op_type = is_const; z_type(opline2->op1.u.constant) = is_long; if(!slash) { zend_error(e_core_error, namespaced name %s should contain slash, z_strval(opline->op1.u.constant)); } /* this is the length of namespace prefix */ z_lval(opline2->op1.u.constant) = prefix_len; /* this is the hash of the non-prefixed part, lowercased */ opline2->extended_value = zend_hash_func(slash+1, name_len+1); set_unused(opline2->op2); } else { // 第一条指令是zend_init_fcall_by_name opline->opcode = zend_init_fcall_by_name; opline->op2 = *function_name; // 先调用,再定义 if (opline->op2.op_type == is_const) { opline->op1.op_type = is_const; z_type(opline->op1.u.constant) = is_string; z_strval(opline->op1.u.constant) = zend_str_tolower_dup(z_strval(opline->op2.u.constant), z_strlen(opline->op2.u.constant)); z_strlen(opline->op1.u.constant) = z_strlen(opline->op2.u.constant); opline->extended_value = zend_hash_func(z_strval(opline->op1.u.constant), z_strlen(opline->op1.u.constant) + 1); } // 以变量当函数名来调用 else { opline->extended_value = 0; set_unused(opline->op1); } } // 将null压入cg(function_call_stack) zend_stack_push(&cg(function_call_stack), (void *) &ptr, sizeof(zend_function *)); zend_do_extended_fcall_begin(tsrmls_c);}
ns_call参数取值为0或者1。如果在命名空间中,以shortname调用函数,则ns_call = 1,并且会生成2条指令。如果是先调用再定义,或者以变量作函数名,则ns_call = 0,并且只会生成1条指令。
以ns_call = 1为例:
生成的op指令如下所示:
common.type == zend_user_function && !arg_should_be_sent_by_ref(function_ptr, (zend_uint) offset)) { zend_error(e_deprecated, call-time pass-by-reference has been deprecated; if you would like to pass it by reference, modify the declaration of %s(). if you would like to enable call-time pass-by-reference, you can set allow_call_time_pass_reference to true in your ini file, function_ptr->common.function_name); } else { zend_error(e_deprecated, call-time pass-by-reference has been deprecated); } }1,首先是从cg(function_call_stack)中获取当前参数对应的函数。注意,可能拿到的只是一个null。因为php的语法允许我们先函数调用,再接着对函数进行定义。如前文所述,这种情况下zend_do_begin_function_call中会向cg(function_call_stack)中压入null,同时会产生do_fcall_by_name指令。
2,在传参的语法推导式中,op可能会有3种,分别是zend_send_val、zend_send_var、zend_send_ref。
expr_without_variable { z_lval($$.u.constant) = 1; zend_do_pass_param(&$1, zend_send_val, z_lval($$.u.constant) tsrmls_cc); }variable { z_lval($$.u.constant) = 1; zend_do_pass_param(&$1, zend_send_var, z_lval($$.u.constant) tsrmls_cc); }'&' w_variable { z_lval($$.u.constant) = 1; zend_do_pass_param(&$2, zend_send_ref, z_lval($$.u.constant) tsrmls_cc); }
这三种op分别对应的语法是expr_without_variable、variable、'&'w_variable,简单来说就是“不含变量的表达式”、“变量”、“引用”。
zend_do_pass_param会判断,如果用户传递的是引用,但同时在php.ini中配置了形如 allow_call_time_pass_reference = off ,则需要产生一条e_deprecated错误信息,告知用户传递的时候不建议强制写成引用。
其实,还有第4种传参的opcode,即zend_send_var_no_ref。我们接下来会提到。
// 函数已定义,则根据函数的定义,来决定send_by_reference是否传引用if (function_ptr) { if (arg_may_be_sent_by_ref(function_ptr, (zend_uint) offset)) { ... } else { // 要么为0,要么为zend_arg_send_by_ref send_by_reference = arg_should_be_sent_by_ref(function_ptr, (zend_uint) offset) ? zend_arg_send_by_ref : 0; }}// 函数为定义,先统一将send_by_reference置为0else { send_by_reference = 0;}// 如果用户传递的参数,本身就是一次函数调用,则将op改成zend_send_var_no_refif (op == zend_send_var && zend_is_function_or_method_call(param)) { /* method call */ op = zend_send_var_no_ref; send_function = zend_arg_send_function;}// 如果用户传递的参数,是一个表达式,并且结果会产生中间变量,则也将op改成zend_send_var_no_refelse if (op == zend_send_val && (param->op_type & (is_var|is_cv))) { op = zend_send_var_no_ref;}
1,send_by_reference表示根据函数的定义,参数是不是引用。arg_may_be_sent_by_ref和arg_should_be_sent_by_ref两个宏这里就不具体叙述了,感兴趣的朋友可以自己阅读代码。
2,op == zend_send_var对应的是variable,假如参数是一个函数调用,也可能会被编译成variable,但是函数调用并不存在显式定义的变量,所以不能直接编译成send_var指令,因此这里就涉及到了上文提到的第4种opcode,即zend_send_var_no_ref。例如:
3,op == zend_send_val对应的是一个表达式,如果该表达式产生了一个变量作为结果,则也需要将op改成zend_send_var_no_ref。例如:
继续来看zend_do_pass_param:
// 如果根据函数定义需要传递引用,且实际传递的参数是变量,则将op改成zend_send_refif (op!=zend_send_var_no_ref && send_by_reference==zend_arg_send_by_ref) { /* change to passing by reference */ switch (param->op_type) { case is_var: case is_cv: op = zend_send_ref; break; default: zend_error(e_compile_error, only variables can be passed by reference); break; }}// 如果实际传递的参数是变量,调用zend_do_end_variable_parse处理链式调用if (original_op == zend_send_var) { switch (op) { case zend_send_var_no_ref: zend_do_end_variable_parse(param, bp_var_r, 0 tsrmls_cc); break; case zend_send_var: if (function_ptr) { zend_do_end_variable_parse(param, bp_var_r, 0 tsrmls_cc); } else { zend_do_end_variable_parse(param, bp_var_func_arg, offset tsrmls_cc); } break; case zend_send_ref: zend_do_end_variable_parse(param, bp_var_w, 0 tsrmls_cc); break; }}
这里注意param->op_type是传递的参数经过编译得到znode的op_type,如果不属于变量(is_var、is_cv),就直接报错了。举例来说:
function foo(&$a){ print($a);}foo($bar == 1); // 抛错 only variables can be passed by reference
上面 $bar == 1 表达式的编译结果,op_type为is_tmp_var,可以看做一种临时的中间结果,并非is_var,is_cv,因此无法编译成功。看着逻辑有点绕,其实很好理解。因为我们传递引用,实际目的是希望能够在函数中,对这个参数的值进行修改,需要参数是可写的。然而 $bar == 1 产生的中间结果,我们无法做出修改,是只读的。
来看zend_do_pass_param的最后一段:
// 获取下一条zend op指令opline = get_next_op(cg(active_op_array) tsrmls_cc);// extended_value加上不同的附加信息if (op == zend_send_var_no_ref) { if (function_ptr) { opline->extended_value = zend_arg_compile_time_bound | send_by_reference | send_function; } else { opline->extended_value = send_function; }} else { if (function_ptr) { opline->extended_value = zend_do_fcall; } else { opline->extended_value = zend_do_fcall_by_name; }}// 设置opcode、op1、op2等opline->opcode = op;opline->op1 = *param;opline->op2.u.opline_num = offset;set_unused(opline->op2);
上面这段代码生成了一条send指令。如果我们调用函数时候传递了多个参数,则会调用多次zend_do_pass_param,最终会生成多条send指令。
至于指令具体是send_var,send_val,还是send_re,亦或是zend_send_var_no_ref,则依靠zend_do_pass_param中的判断。zend_do_pass_param中的逻辑分支比较多,一下子不能弄明白所有分支也没关系,最重要的是知道它会根据函数的定义以及实际传递的参数,产生最合适的send指令。
还是回到我们开始的例子,对于 foo($bar) ,则经过zend_do_pass_param之后,产生的send指令细节如下:
4、结束编译结束函数调用是通过zend_do_end_function_call来完成的。根据前文所述,zend_do_begin_function_call并不产生一条实际的调用指令,但它确定了最终函数调用走的是do_fcall还是do_fcall_by_name,并且据此来生成zend_init_ns_fcall_by_name或zend_init_fcall_by_name指令。
实际的调用指令是放在zend_do_end_function_call中来生成的。
具体分析下zend_do_end_function_call:
zend_op *opline;// 这段逻辑分支现在已经走不到了if (is_method && function_name && function_name->op_type == is_unused) { /* clone */ if (z_lval(argument_list->u.constant) != 0) { zend_error(e_warning, clone method does not require arguments); } opline = &cg(active_op_array)->opcodes[z_lval(function_name->u.constant)];} else { opline = get_next_op(cg(active_op_array) tsrmls_cc); // 函数,名称确定,非dynamic_fcall,函数则生成zend_do_fcall指令 if (!is_method && !is_dynamic_fcall && function_name->op_type==is_const) { opline->opcode = zend_do_fcall; opline->op1 = *function_name; zval_long(&opline->op2.u.constant, zend_hash_func(z_strval(function_name->u.constant), z_strlen(function_name->u.constant) + 1)); } // 否则生成zend_do_fcall_by_name指令 else { opline->opcode = zend_do_fcall_by_name; set_unused(opline->op1); }}// 生成临时变量索引,函数的调用,返回的znode必然是is_varopline->result.u.var = get_temporary_variable(cg(active_op_array));opline->result.op_type = is_var;*result = opline->result;set_unused(opline->op2);// 从cg(function_call_stack)弹出当前被调用的函数zend_stack_del_top(&cg(function_call_stack));// 传参个数opline->extended_value = z_lval(argument_list->u.constant);
其中有一段if逻辑分支已经走不到了,可以忽略。
具体考据:这段逻辑在462eff3中被添加,主要用于当调用__clone魔术方法时传参进行抛错,但在8e30d96中,已经不允许直接调用__clone方法了,在进入zend_do_end_function_call之前便会终止编译,所以实际上已经再也走不到该分支了。
直接看else部分,else生成了一条zend op指令。如果函数名确定,函数已被定义,并且不属于动态调用等,则生成的op指令为zend_do_fcall,否则生成zend_do_fcall_by_name。对于zend_do_fcall指令,其操作数比较明确,为函数名,但是对于zend_do_fcall_by_name来说,由于被调的函数尚未明确,所以将操作数置为unused。
5、总结用一张图总结一下函数调用大致的编译流程:
红色的方框为生成的op指令。特别是编译传参的地方,情况比较多,可能会产出4种send指令。
http://www.bkjia.com/phpjc/1133568.htmlwww.bkjia.comtruehttp://www.bkjia.com/phpjc/1133568.htmltecharticle深入剖析php执行原理(4):函数的调用,深入剖析php 本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的。 我们前...
其它类似信息

推荐信息