本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的。
我们前面的章节弄明白了函数体会被编译生成哪些zend_op指令,本章会研究函数调用语句会生成哪些zend_op指,等后面的章节再根据这些op指令,来剖析php运行时的细节。
源码依然取自php5.3.29。
函数调用
回顾之前用的php代码示例:
1 2 3 4 5 6 7 8 | <span style= "color: #000000;" >php
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
foo(</span><span style= "color: #800080;" > $bar </span>);
|
登录后复制
在函数编译一章里已经分析过,函数foo最终会编译生成对应的zend_function,存放于函数表(CG(function_table))中。
现在开始看 foo($bar); 一句,这应该是最简单的函数调用语句了。其他还有一些形式更为复杂的函数调用,例如以可变变量作为函数名,例如导入的函数以别名进行调用(涉及到命名空间),再例如以引用作为参数,以表达式作为参数,以函数调用本身作为参数等等。
我们从简单的来入手,弄清楚调用语句的编译过程及产出,对于复杂的一些调用,下文也争取都能谈到一些。
1、语法推导
就 foo($bar); 而言,其主要部分语法树为:

绿色的节点表示最后对应到php代码中的字面。红色的部分是语法推导过程中最重要的几步,特别是function_call。
我们从语法分析文件zend_language_parser.y中挑出相关的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function_call:
namespace_name '(' { $2 .u.opline_num = zend_do_begin_function_call(& $1 , 1 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(& $1 , &$$, & $4 , 0, $2 .u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
| T_NAMESPACE T_NS_SEPARATOR namespace_name '(' { $1 .op_type = IS_CONST; ZVAL_EMPTY_STRING(& $1 .u.constant); zend_do_build_namespace_name(& $1 , & $1 , & $3 TSRMLS_CC); $4 .u.opline_num = zend_do_begin_function_call(& $1 , 0 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(& $1 , &$$, & $6 , 0, $4 .u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
| T_NS_SEPARATOR namespace_name '(' { $3 .u.opline_num = zend_do_begin_function_call(& $2 , 0 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(& $2 , &$$, & $5 , 0, $3 .u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
| class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' { $4 .u.opline_num = zend_do_begin_class_member_function_call(& $1 , & $3 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call( $4 .u.opline_num?NULL:& $3 , &$$, & $6 , $4 .u.opline_num, $4 .u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
| class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' { zend_do_end_variable_parse(& $3 , BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_class_member_function_call(& $1 , & $3 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(NULL, &$$, & $6 , 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
| variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' { zend_do_begin_class_member_function_call(& $1 , & $3 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(NULL, &$$, & $6 , 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
| variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' { zend_do_end_variable_parse(& $3 , BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_class_member_function_call(& $1 , & $3 TSRMLS_CC); }
function_call_parameter_list
')' { zend_do_end_function_call(NULL, &$$, & $6 , 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
| variable_without_objects '(' { zend_do_end_variable_parse(& $1 , BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_dynamic_function_call(& $1 , 0 TSRMLS_CC); }
function_call_parameter_list ')'
{ zend_do_end_function_call(& $1 , &$$, & $4 , 0, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
;<br><br>function_call_parameter_list:<br> non_empty_function_call_parameter_list { $$ = $1 ; }<br> | { Z_LVAL($$.u.constant) = 0; }<br>;<br><br><br>non_empty_function_call_parameter_list:<br> expr_without_variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(& $1 , ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }<br> | variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(& $1 , ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }<br> | '&' w_variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(& $2 , ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }<br> | non_empty_function_call_parameter_list ',' expr_without_variable { Z_LVAL($$.u.constant)=Z_LVAL( $1 .u.constant)+1; zend_do_pass_param(& $3 , ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }<br> | non_empty_function_call_parameter_list ',' variable { Z_LVAL($$.u.constant)=Z_LVAL( $1 .u.constant)+1; zend_do_pass_param(& $3 , ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }<br> | non_empty_function_call_parameter_list ',' '&' w_variable { Z_LVAL($$.u.constant)=Z_LVAL( $1 .u.constant)+1; zend_do_pass_param(& $4 , ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }<br>;
|
登录后复制
其结构并不复杂:
1)function_call这条推导,代表了一个完整的函数调用。
2)namespace_name是指经过命名空间修饰过之后的函数名,由于我们的例子中,函数foo并没有处于任何一个命名空间里,所以namespace_name其实就是foo。如果我们的函数定义在命名空间中,则namespace_name是一个类似“全路径”的fullname。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span style= "color: #000000;" > namespace MyProject
{
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
}
namespace
{
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;</span>
MyProject\foo(<span style= "color: #800080;" > $bar </span><span style= "color: #000000;" >);<span style= "color: #008000;" >
}</span>
|
登录后复制
3)function_call_parameter_list是函数的参数列表,而non_empty_function_call_parameter_list则代表了非空参数列表。
4)从这些推导产生式里,我们还能看出编译时的所运用的一些关键处理:
1 | zend_do_begin_function_call-->zend_do_pass_param-->zend_do_end_function_call<br><br> 开始 解析参数 结束
|
登录后复制
和编译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
代码注解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | zend_function *<span style= "color: #000000;" > function ;
</span><span style= "color: #0000ff;" >char</span> *<span style= "color: #000000;" >lcname;
</span><span style= "color: #0000ff;" >char</span> *is_compound = memchr(Z_STRVAL(function_name->u.constant), <span style= "color: #800000;" > '</span><span style="color: #800000;">\\</span><span style="color: #800000;">' </span>, Z_STRLEN(function_name-><span style= "color: #000000;" >u.constant));
</span><span style= "color: #008000;" >
<span style= "color: #000000;" >zend_resolve_non_class_name(function_name, check_namespace TSRMLS_CC);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (check_namespace && CG(current_namespace) && !<span style= "color: #000000;" >is_compound) {
</span><span style= "color: #008000;" >
</span>
<span style= "color: #008000;" >
</span><span style= "color: #000000;" >
zend_do_begin_dynamic_function_call(function_name, </span><span style= "color: #800080;" >1</span><span style= "color: #000000;" > TSRMLS_CC);
</span><span style= "color: #0000ff;" > return </span> <span style= "color: #800080;" >1</span><span style= "color: #000000;" >;
}
</span><span style= "color: #008000;" >
lcname = zend_str_tolower_dup(function_name->u.constant.value.str.val, function_name-><span style= "color: #000000;" >u.constant.value.str.len);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> ((zend_hash_find(CG(function_table), lcname, function_name->u.constant.value.str.len+<span style= "color: #800080;" >1</span>, (<span style= "color: #0000ff;" >void</span> **) & function ) == FAILURE) ||<span style= "color: #000000;" >
((CG(compiler_options) </span>& ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS) && ( function ->type ==<span style= "color: #000000;" > ZEND_INTERNAL_FUNCTION))) {
zend_do_begin_dynamic_function_call(function_name, </span><span style= "color: #800080;" >0</span><span style= "color: #000000;" > TSRMLS_CC);
efree(lcname);
</span><span style= "color: #0000ff;" > return </span> <span style= "color: #800080;" >1</span>; <span style= "color: #008000;" > </span><span style= "color: #000000;" >
}
efree(function_name</span>-><span style= "color: #000000;" >u.constant.value.str.val);
function_name</span>->u.constant.value.str.val =<span style= "color: #000000;" > lcname;
</span><span style= "color: #008000;" >
zend_stack_push(&CG(function_call_stack), (<span style= "color: #0000ff;" >void</span> *) & function , <span style= "color: #0000ff;" >sizeof</span>(zend_function *<span style= "color: #000000;" >));
zend_do_extended_fcall_begin(TSRMLS_C);
</span><span style= "color: #0000ff;" > return </span> <span style= "color: #800080;" >0</span>;
|
登录后复制
有几点需要理解的:
1,zend_resolve_non_class_name。由于php支持命名空间、也支持别名/导入等特性,因此首先要做的是将函数名称进行修正,否则在CG(function_table)中找不到。例如,函数处于一个命名空间中,则可能需要将函数名添加上命名空间作为前缀,最终形成完整的函数名,也就是我们前文提到的以一种类似“全路径”的fullname作为函数名。再例如,函数名只是一个设置的别名,它实际指向了另一个命名空间中的某个函数,则需要将其改写成真正被调用函数的名称。这些工作,均由zend_resolve_non_class_name完成。命名空间添加了不少复杂度,下面是一些简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 | <span style= "color: #000000;" >php
namespace MyProject;
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;<br>
foo(</span><span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
namespace \foo(</span><span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
\MyProject\foo(</span><span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
|
登录后复制
总之,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我们下面再具体描述。
1 2 3 4 5 6 7 8 9 10 11 | <span style= "color: #000000;" >php
namespace MyProject\sub;
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
namespace MyProject;
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
sub\foo(</span><span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
|
登录后复制
注意上述例子,我们以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允许我们先调用,再去定义函数。例如:
1 2 3 4 5 6 7 8 9 10 11 | <span style= "color: #000000;" >php
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
</span><span style= "color: #008000;" >
foo(<span style= "color: #800080;" > $bar </span><span style= "color: #000000;" >);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}</span>
|
登录后复制
4,在zend_do_begin_function_call的最后,我们将函数压入CG(function_call_stack)。这是一个栈,因为在后续对传参的编译,我们仍然需要用到函数,所以这里将其压亚入栈中,方便后面获取使用。之所以用栈,是因为调用函数传递的参数,可能是另一次函数调用。为了确保参数总是能找到对应的函数,所以用栈。
1 2 3 4 5 6 7 8 | <span style= "color: #000000;" >php
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
foo(</span><span style= "color: #008080;" > strlen </span>(<span style= "color: #800080;" > $bar </span>)); <span style= "color: #008000;" >
|
登录后复制
2.2、 zend_do_begin_dynamic_function_call
前面提到,正常的调用,会先执行zend_do_begin_function_call,在zend_do_begin_function_call中有两种情况会进一步调用zend_do_begin_dynamic_function_call来处理。
一是,在命名空间中,以shortname调用函数;
二是,在调用函数时,尚未定义函数。
其实还有第三种情况会走到zend_do_begin_dynamic_function_call,就是当我们调用函数的时候,函数名并非直接写成字面,而是通过变量等形式来间接确定。这种情况下,zend vm会直接执行zend_do_begin_dynamic_function_call。
举例1:
1 2 3 4 5 6 7 8 9 | <span style= "color: #000000;" >php
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $func </span> = 'foo' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $func </span>(<span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
|
登录后复制
此时, $func($bar) 对应function_call语法推导式的最后一条:
1 2 3 4 | function_call:<br> ...
| variable_without_objects '(' { zend_do_end_variable_parse(& $1 , BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_dynamic_function_call(& $1 , 0 TSRMLS_CC); }
function_call_parameter_list ')'
{ zend_do_end_function_call(& $1 , &$$, & $4 , 0, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
|
登录后复制
推导式中的variable_without_objects对应的就是变量 $func 。$func其实是一个compiled_variable,并且在op_array->vars数组中索引为1,索引为0的是在它之前定义的变量 $bar 。
举例2:
1 2 3 4 5 6 7 8 9 | <span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $func </span> = 'foo' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $ref_func </span> = 'func' <span style= "color: #000000;" >;
$</span><span style= "color: #800080;" > $ref_func </span>(<span style= "color: #800080;" > $bar </span>); <span style= "color: #008000;" >
|
登录后复制
该例是以可变变量来调用函数,和例1一样, $$ref_func($bar)也是对应function_call语法推导式的最后一条,所以不会走进zend_do_begin_function_call,而是直接进入zend_do_begin_dynamic_function_call。不同的点在于 $$ref_func 节点类型不再是compiled_variable,而是普通的variable,标识为IS_VAR。
下面的图画出了5种case,第1种不经过zend_do_begin_dynamic_function_call,而后4种会调用zend_do_begin_dynamic_function_call处理,注意最后2种不经过zend_do_begin_function_call:

具体看下zend_do_begin_dynamic_function_call的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | <span style= "color: #0000ff;" >void</span> zend_do_begin_dynamic_function_call(znode *function_name, <span style= "color: #0000ff;" >int</span> ns_call TSRMLS_DC) <span style= "color: #008000;" > </span><span style= "color: #000000;" >
{
unsigned </span><span style= "color: #0000ff;" >char</span> *ptr =<span style= "color: #000000;" > NULL;
zend_op </span>*opline, *<span style= "color: #000000;" >opline2;
</span><span style= "color: #008000;" >
opline =<span style= "color: #000000;" > get_next_op(CG(active_op_array) TSRMLS_CC);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (ns_call) {
</span><span style= "color: #0000ff;" >char</span> *<span style= "color: #000000;" >slash;
</span><span style= "color: #0000ff;" >int</span><span style= "color: #000000;" > prefix_len, name_len;
</span><span style= "color: #008000;" >
</span>
<span style= "color: #008000;" >
opline->opcode =<span style= "color: #000000;" > ZEND_INIT_NS_FCALL_BY_NAME;
opline</span>->op2 = *<span style= "color: #000000;" >function_name;
opline</span>->extended_value = <span style= "color: #800080;" >0</span><span style= "color: #000000;" >;
opline</span>->op1.op_type =<span style= "color: #000000;" > IS_CONST;
Z_TYPE(opline</span>->op1.u.constant) =<span style= "color: #000000;" > IS_STRING ;
Z_STRVAL(opline</span>->op1.u.constant) = zend_str_tolower_dup(Z_STRVAL(opline->op2.u.constant), Z_STRLEN(opline-><span style= "color: #000000;" >op2.u.constant));
Z_STRLEN(opline</span>->op1.u.constant) = Z_STRLEN(opline-><span style= "color: #000000;" >op2.u.constant);
opline</span>->extended_value = zend_hash_func(Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant) + <span style= "color: #800080;" >1</span><span style= "color: #000000;" >);
</span><span style= "color: #008000;" >
slash = zend_memrchr(Z_STRVAL(opline->op1.u.constant), <span style= "color: #800000;" > '</span><span style="color: #800000;">\\</span><span style="color: #800000;">' </span>, Z_STRLEN(opline-><span style= "color: #000000;" >op1.u.constant));
prefix_len </span>= slash-Z_STRVAL(opline->op1.u.constant)+<span style= "color: #800080;" >1</span><span style= "color: #000000;" >;
name_len </span>= Z_STRLEN(opline->op1.u.constant)-<span style= "color: #000000;" >prefix_len;
opline2 </span>=<span style= "color: #000000;" > get_next_op(CG(active_op_array) TSRMLS_CC);
opline2</span>->opcode =<span style= "color: #000000;" > ZEND_OP_DATA;
opline2</span>->op1.op_type =<span style= "color: #000000;" > IS_CONST;
Z_TYPE(opline2</span>->op1.u.constant) =<span style= "color: #000000;" > IS_LONG ;
</span><span style= "color: #0000ff;" > if </span>(!<span style= "color: #000000;" >slash) {
zend_error(E_CORE_ERROR, </span><span style= "color: #800000;" > "</span><span style=" color: #800000; ">Namespaced name %s should contain slash</span><span style=" color: #800000; ">" </span>, Z_STRVAL(opline-><span style= "color: #000000;" >op1.u.constant));
}
</span><span style= "color: #008000;" > </span><span style= "color: #000000;" >
Z_LVAL(opline2</span>->op1.u.constant) =<span style= "color: #000000;" > prefix_len;
</span><span style= "color: #008000;" > </span><span style= "color: #000000;" >
opline2</span>->extended_value = zend_hash_func(slash+<span style= "color: #800080;" >1</span>, name_len+<span style= "color: #800080;" >1</span><span style= "color: #000000;" >);
SET_UNUSED(opline2</span>-><span style= "color: #000000;" >op2);
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
</span><span style= "color: #008000;" >
opline->opcode =<span style= "color: #000000;" > ZEND_INIT_FCALL_BY_NAME;
opline</span>->op2 = *<span style= "color: #000000;" >function_name;
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (opline->op2.op_type ==<span style= "color: #000000;" > IS_CONST) {
opline</span>->op1.op_type =<span style= "color: #000000;" > IS_CONST;
Z_TYPE(opline</span>->op1.u.constant) =<span style= "color: #000000;" > IS_STRING ;
Z_STRVAL(opline</span>->op1.u.constant) = zend_str_tolower_dup(Z_STRVAL(opline->op2.u.constant), Z_STRLEN(opline-><span style= "color: #000000;" >op2.u.constant));
Z_STRLEN(opline</span>->op1.u.constant) = Z_STRLEN(opline-><span style= "color: #000000;" >op2.u.constant);
opline</span>->extended_value = zend_hash_func(Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant) + <span style= "color: #800080;" >1</span><span style= "color: #000000;" >);
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
opline</span>->extended_value = <span style= "color: #800080;" >0</span><span style= "color: #000000;" >;
SET_UNUSED(opline</span>-><span style= "color: #000000;" >op1);
}
}
</span><span style= "color: #008000;" >
zend_stack_push(&CG(function_call_stack), (<span style= "color: #0000ff;" >void</span> *) &ptr, <span style= "color: #0000ff;" >sizeof</span>(zend_function *<span style= "color: #000000;" >));
zend_do_extended_fcall_begin(TSRMLS_C);
}</span>
|
登录后复制
ns_call参数取值为0或者1。如果在命名空间中,以shortname调用函数,则ns_call = 1,并且会生成2条指令。如果是先调用再定义,或者以变量作函数名,则ns_call = 0,并且只会生成1条指令。
以ns_call = 1为例:
1 2 3 4 5 6 7 8 9 10 | <span style= "color: #000000;" >php
namespace MyProject;
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
foo(</span><span style= "color: #800080;" > $bar </span>);
|
登录后复制
生成的op指令如下所示:

以ns_call = 0,先调用再定义为例:
1 2 3 4 5 6 7 8 | <span style= "color: #000000;" >php
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
foo(</span><span style= "color: #800080;" > $bar </span><span style= "color: #000000;" >);
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}</span>
|
登录后复制
生成的op指令如下所示:

以ns_call = 0,变量作为函数名为例:
1 2 3 4 5 6 7 8 9 | <span style= "color: #000000;" >php
</span><span style= "color: #0000ff;" > function </span> foo(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $arg1 </span><span style= "color: #000000;" >);
}
</span><span style= "color: #800080;" > $bar </span> = 'hello php' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $func </span> = 'foo' <span style= "color: #000000;" >;
</span><span style= "color: #800080;" > $func </span>(<span style= "color: #800080;" > $bar </span>);
|
登录后复制
生成的op指令如下所示:

上面一共新出现了3条op指令:ZEND_INIT_NS_FCALL_BY_NAME、ZEND_OP_DATA以及ZEND_INIT_FCALL_BY_NAME。
其中,ZEND_INIT_NS_FCALL_BY_NAME和ZEND_INIT_FCALL_BY_NAME都是在运行时从函数表中取出真正被调用的函数。ZEND_OP_DATA在本case中并不起作用,第5章中会具体分析ZEND_OP_DATA。
回到zend_do_begin_dynamic_function_call,在代码的最后向CG(function_call_stack)压入了一个NULL。CG(function_call_stack)在随后的zend_do_pass_param中会有作用,这里压入NULL,意味着随后zend_do_pass_param中会取出NULL,表明无法从函数定义中判断其参数的属性(是否为引用传递等)。
3、编译传参
从前面语法推导式里可以看出,调用函数时的传参,最终由zend_do_pass_param来完成。具体参数该怎么传,实际情况是很复杂的。php语法比较松散,可以传递正常的变量,也可以传递表达式,可以传递引用,甚至可以传递另一个函数调用。
但无论是哪种情况,最终传参逻辑都会编译成类似SEND_VAR,SEND_VAL,SEND_REF,ZEND_SEND_VAR_NO_REF等指令,这些指令和函数中的RECV指令是对应的。具体来说,zend vm进入执行期之后,一般都是会通过SEND_XXX等指令发送参数,然后执行DO_FCALL/DO_FCALL_BY_NAME等指令开始调用函数。进入函数体内之后,再执行RECV完成参数的接收。第2章中我们具体讲解了RECV指令,除非函数不接受参数,否则RECV必定是函数体内的第一条指令。
如下图所示:

我们给出的示例是一种最简单的情况,也就是传递一个正常的变量。
来看下zend_do_pass_param的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <span style= "color: #0000ff;" >void</span> zend_do_pass_param(znode *param, zend_uchar op, <span style= "color: #0000ff;" >int</span> offset TSRMLS_DC) <span style= "color: #008000;" > </span><span style= "color: #000000;" >
{
zend_op </span>*<span style= "color: #000000;" >opline;
</span><span style= "color: #0000ff;" >int</span> original_op =<span style= "color: #000000;" > op;
zend_function </span>**function_ptr_ptr, *<span style= "color: #000000;" >function_ptr;
</span><span style= "color: #0000ff;" >int</span><span style= "color: #000000;" > send_by_reference;
</span><span style= "color: #0000ff;" >int</span> send_function = <span style= "color: #800080;" >0</span><span style= "color: #000000;" >;
</span><span style= "color: #008000;" >
zend_stack_top(&CG(function_call_stack), (<span style= "color: #0000ff;" >void</span> **) &<span style= "color: #000000;" >function_ptr_ptr);
function_ptr </span>= *<span style= "color: #000000;" >function_ptr_ptr;
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (original_op == ZEND_SEND_REF && !<span style= "color: #000000;" >CG(allow_call_time_pass_reference)) {
</span><span style= "color: #0000ff;" > if </span> (function_ptr &&<span style= "color: #000000;" >
function_ptr</span>->common.function_name &&<span style= "color: #000000;" >
function_ptr</span>->common.type == ZEND_USER_FUNCTION &&
!<span style= "color: #000000;" >ARG_SHOULD_BE_SENT_BY_REF(function_ptr, (zend_uint) offset)) {
zend_error(E_DEPRECATED,
</span><span style= "color: #800000;" > "</span><span style=" color: #800000; ">Call-time pass-by-reference has been deprecated; </span><span style=" color: #800000; ">" </span>
<span style= "color: #800000;" > "</span><span style=" color: #800000; ">If you would like to pass it by reference, modify the declaration of %s(). </span><span style=" color: #800000; ">" </span>
<span style= "color: #800000;" > "</span><span style=" color: #800000; ">If you would like to enable call-time pass-by-reference, you can set </span><span style=" color: #800000; ">" </span>
<span style= "color: #800000;" > "</span><span style=" color: #800000; ">allow_call_time_pass_reference to true in your INI file</span><span style=" color: #800000; ">" </span>, function_ptr-><span style= "color: #000000;" >common.function_name);
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
zend_error(E_DEPRECATED, </span><span style= "color: #800000;" > "</span><span style=" color: #800000; ">Call-time pass-by-reference has been deprecated</span><span style=" color: #800000; ">" </span><span style= "color: #000000;" >);
}
}</span>
|
登录后复制
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。
1 2 3 | expr_without_variable { Z_LVAL($$.u.constant) = <span style= "color: #800080;" >1</span>; zend_do_pass_param(&$<span style= "color: #800080;" >1</span><span style= "color: #000000;" >, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
variable { Z_LVAL($$.u.constant) </span>= <span style= "color: #800080;" >1</span>; zend_do_pass_param(&$<span style= "color: #800080;" >1</span><span style= "color: #000000;" >, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
</span><span style= "color: #800000;" > '</span><span style="color: #800000;">&</span><span style="color: #800000;">' </span> w_variable { Z_LVAL($$.u.constant) = <span style= "color: #800080;" >1</span>; zend_do_pass_param(&$<span style= "color: #800080;" >2</span>, 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。我们接下来会提到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (function_ptr) {
</span><span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (ARG_MAY_BE_SENT_BY_REF(function_ptr, (zend_uint) offset)) {
...
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
</span><span style= "color: #008000;" >
send_by_reference = ARG_SHOULD_BE_SENT_BY_REF(function_ptr, (zend_uint) offset) ? ZEND_ARG_SEND_BY_REF : <span style= "color: #800080;" >0</span><span style= "color: #000000;" >;
}
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
send_by_reference </span>= <span style= "color: #800080;" >0</span><span style= "color: #000000;" >;
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (op == ZEND_SEND_VAR &&<span style= "color: #000000;" > zend_is_function_or_method_call(param)) {
</span><span style= "color: #008000;" > </span><span style= "color: #000000;" >
op </span>=<span style= "color: #000000;" > ZEND_SEND_VAR_NO_REF;
send_function </span>=<span style= "color: #000000;" > ZEND_ARG_SEND_FUNCTION;
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > else </span> <span style= "color: #0000ff;" > if </span> (op == ZEND_SEND_VAL && (param->op_type & (IS_VAR|<span style= "color: #000000;" >IS_CV))) {
op </span>=<span style= "color: #000000;" > ZEND_SEND_VAR_NO_REF;
}</span>
|
登录后复制
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (op!=ZEND_SEND_VAR_NO_REF && send_by_reference==<span style= "color: #000000;" >ZEND_ARG_SEND_BY_REF) {
</span><span style= "color: #008000;" > </span>
<span style= "color: #0000ff;" > switch </span> (param-><span style= "color: #000000;" >op_type) {
</span><span style= "color: #0000ff;" > case </span><span style= "color: #000000;" > IS_VAR:
</span><span style= "color: #0000ff;" > case </span><span style= "color: #000000;" > IS_CV:
op </span>=<span style= "color: #000000;" > ZEND_SEND_REF;
</span><span style= "color: #0000ff;" > break </span><span style= "color: #000000;" >;
</span><span style= "color: #0000ff;" > default </span><span style= "color: #000000;" >:
zend_error(E_COMPILE_ERROR, </span><span style= "color: #800000;" > "</span><span style=" color: #800000; ">Only variables can be passed by reference</span><span style=" color: #800000; ">" </span><span style= "color: #000000;" >);
</span><span style= "color: #0000ff;" > break </span><span style= "color: #000000;" >;
}
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (original_op ==<span style= "color: #000000;" > ZEND_SEND_VAR) {
</span><span style= "color: #0000ff;" > switch </span><span style= "color: #000000;" > (op) {
</span><span style= "color: #0000ff;" > case </span><span style= "color: #000000;" > ZEND_SEND_VAR_NO_REF:
zend_do_end_variable_parse(param, BP_VAR_R, </span><span style= "color: #800080;" >0</span><span style= "color: #000000;" > TSRMLS_CC);
</span><span style= "color: #0000ff;" > break </span><span style= "color: #000000;" >;
</span><span style= "color: #0000ff;" > case </span><span style= "color: #000000;" > ZEND_SEND_VAR:
</span><span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (function_ptr) {
zend_do_end_variable_parse(param, BP_VAR_R, </span><span style= "color: #800080;" >0</span><span style= "color: #000000;" > TSRMLS_CC);
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
zend_do_end_variable_parse(param, BP_VAR_FUNC_ARG, offset TSRMLS_CC);
}
</span><span style= "color: #0000ff;" > break </span><span style= "color: #000000;" >;
</span><span style= "color: #0000ff;" > case </span><span style= "color: #000000;" > ZEND_SEND_REF:
zend_do_end_variable_parse(param, BP_VAR_W, </span><span style= "color: #800080;" >0</span><span style= "color: #000000;" > TSRMLS_CC);
</span><span style= "color: #0000ff;" > break </span><span style= "color: #000000;" >;
}
}</span>
|
登录后复制
这里注意param->op_type是传递的参数经过编译得到znode的op_type,如果不属于变量(IS_VAR、IS_CV),就直接报错了。举例来说:
1 2 3 4 5 6 | <span style= "color: #0000ff;" > function </span> foo(&<span style= "color: #800080;" > $a </span><span style= "color: #000000;" >)
{
</span><span style= "color: #0000ff;" > print </span>(<span style= "color: #800080;" > $a </span><span style= "color: #000000;" >);
}
foo(</span><span style= "color: #800080;" > $bar </span> == 1);
|
登录后复制
上面 $bar == 1 表达式的编译结果,op_type为IS_TMP_VAR,可以看做一种临时的中间结果,并非IS_VAR,IS_CV,因此无法编译成功。看着逻辑有点绕,其实很好理解。因为我们传递引用,实际目的是希望能够在函数中,对这个参数的值进行修改,需要参数是可写的。然而 $bar == 1 产生的中间结果,我们无法做出修改,是只读的。
来看zend_do_pass_param的最后一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <span style= "color: #008000;" >
opline =<span style= "color: #000000;" > get_next_op(CG(active_op_array) TSRMLS_CC);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (op ==<span style= "color: #000000;" > ZEND_SEND_VAR_NO_REF) {
</span><span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (function_ptr) {
opline</span>->extended_value = ZEND_ARG_COMPILE_TIME_BOUND | send_by_reference |<span style= "color: #000000;" > send_function;
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
opline</span>->extended_value =<span style= "color: #000000;" > send_function;
}
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
</span><span style= "color: #0000ff;" > if </span><span style= "color: #000000;" > (function_ptr) {
opline</span>->extended_value =<span style= "color: #000000;" > ZEND_DO_FCALL;
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
opline</span>->extended_value =<span style= "color: #000000;" > ZEND_DO_FCALL_BY_NAME;
}
}
</span><span style= "color: #008000;" >
opline->opcode =<span style= "color: #000000;" > op;
opline</span>->op1 = *<span style= "color: #000000;" >param;
opline</span>->op2.u.opline_num =<span style= "color: #000000;" > offset;
SET_UNUSED(opline</span>->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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | zend_op *<span style= "color: #000000;" >opline;
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (is_method && function_name && function_name->op_type ==<span style= "color: #000000;" > IS_UNUSED) {
</span><span style= "color: #008000;" > </span>
<span style= "color: #0000ff;" > if </span> (Z_LVAL(argument_list->u.constant) != <span style= "color: #800080;" >0</span><span style= "color: #000000;" >) {
zend_error(E_WARNING, </span><span style= "color: #800000;" > "</span><span style=" color: #800000; ">Clone method does not require arguments</span><span style=" color: #800000; ">" </span><span style= "color: #000000;" >);
}
opline </span>= &CG(active_op_array)->opcodes[Z_LVAL(function_name-><span style= "color: #000000;" >u.constant)];
} </span><span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
opline </span>=<span style= "color: #000000;" > get_next_op(CG(active_op_array) TSRMLS_CC);
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > if </span> (!is_method && !is_dynamic_fcall && function_name->op_type==<span style= "color: #000000;" >IS_CONST) {
opline</span>->opcode =<span style= "color: #000000;" > ZEND_DO_FCALL;
opline</span>->op1 = *<span style= "color: #000000;" >function_name;
ZVAL_LONG(</span>&opline->op2.u.constant, zend_hash_func(Z_STRVAL(function_name->u.constant), Z_STRLEN(function_name->u.constant) + <span style= "color: #800080;" >1</span><span style= "color: #000000;" >));
}
</span><span style= "color: #008000;" >
<span style= "color: #0000ff;" > else </span><span style= "color: #000000;" > {
opline</span>->opcode =<span style= "color: #000000;" > ZEND_DO_FCALL_BY_NAME;
SET_UNUSED(opline</span>-><span style= "color: #000000;" >op1);
}
}
</span><span style= "color: #008000;" >
opline->result.u.<span style= "color: #0000ff;" > var </span> =<span style= "color: #000000;" > get_temporary_variable(CG(active_op_array));
opline</span>->result.op_type =<span style= "color: #000000;" > IS_VAR;
</span>*result = opline-><span style= "color: #000000;" >result;
SET_UNUSED(opline</span>-><span style= "color: #000000;" >op2);
</span><span style= "color: #008000;" >
zend_stack_del_top(&<span style= "color: #000000;" >CG(function_call_stack));
</span><span style= "color: #008000;" >
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指令。