深⼊剖析php执⾏原理(4):函数的调⽤
本章开始研究php中函数的调⽤和执⾏,先来看函数调⽤语句是如何被编译的。
我们前⾯的章节弄明⽩了函数体会被编译⽣成哪些zend_op指令,本章会研究函数调⽤语句会⽣成哪些zend_op指,等后⾯的章节再根据这些op指令,来剖析php运⾏时的细
节。
源码依然取⾃php5.3.29。
函数调⽤
回顾之前⽤的php代码⽰例:
<?php
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
foo($bar);
在⼀章⾥已经分析过,函数foo最终会编译⽣成对应的zend_function,存放于函数表(CG(function_table))中。
现在开始看 foo($bar); ⼀句,这应该是最简单的函数调⽤语句了。其他还有⼀些形式更为复杂的函数调⽤,例如以可变变量作为函数名,例如导⼊的函数以别名进⾏调⽤(涉及
到命名空间),再例如以引⽤作为参数,以表达式作为参数,以函数调⽤本⾝作为参数等等。
我们从简单的来⼊⼿,弄清楚调⽤语句的编译过程及产出,对于复杂的⼀些调⽤,下⽂也争取都能谈到⼀些。
1、语法推导
就 foo($bar); ⽽⾔,其主要部分语法树为:
绿⾊的节点表⽰最后对应到php代码中的字⾯。红⾊的部分是语法推导过程中最重要的⼏步,特别是function_call。
我们从语法分析⽂件zend_language_parser.y中挑出相关的:
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(&$stant); zend_do_build_namespace_name(&$1, &$1, &$3 TSRMLS_CC); $4.u.opline_num = zend_do_begin_function_c 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);}
;
function_call_parameter_list:
non_empty_function_call_parameter_list { $$ = $1; }
| /* empty */ { Z_LVAL($$.u.constant) = 0; }
;
non_empty_function_call_parameter_list:
expr_without_variable { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$1, ZEND_SEND_VA
L, 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); }
| non_empty_function_call_parameter_list ',' expr_without_variable { Z_LVAL($$.u.constant)=Z_LVAL($stant)+1; zend_do_pass_param(&$3, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
| non_empty_function_call_parameter_list ',' variable { Z_LVAL($$.u.constant)=Z_LVAL($stant)+1; zend_do_pass_param(&$3, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
| non_empty_function_call_parameter_list ',' '&' w_variable { Z_LVAL($$.u.constant)=Z_LVAL($stant)+1; zend_do_pass_param(&$4, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); } ;
其结构并不复杂:
1)function_call这条推导,代表了⼀个完整的函数调⽤。
2)namespace_name是指经过命名空间修饰过之后的函数名,由于我们的例⼦中,函数foo并没有处于任何⼀个命名空间⾥,所以namespace_name其实就是foo。如果我们的函数定义在命名空间中,则namespace_name是⼀个类似“全路径”的fullname。
namespace MyProject
{
function foo($arg1)
{
print($arg1);
}
}
namespace
{
$bar = 'hello php';
MyProject\foo($bar);// 以类似“全路径”的fullname来调⽤函数,则namespace_name为MyProject\foo
}
3)function_call_parameter_list是函数的参数列表,⽽non_empty_function_call_parameter_list则代表了⾮空参数列表。
4)从这些推导产⽣式⾥,我们还能看出编译时的所运⽤的⼀些关键处理:
zend_do_begin_function_call-->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-&stant), '\\', Z_STRLEN(function_name-&stant));
// 将函数名进⾏修正,例如带上命名空间作为前缀等
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);
return1;
}
// 转成⼩写,因为CG(function_table)中的函数名都是⼩写
lcname = zend_str_tolower_dup(function_name-&stant.value.str.val, function_name-&stant.value.str.len);
// 如果function_table中不到该函数,则也尝试⽣成DO_FCALL_BY_NAME指令
if ((zend_hash_find(CG(function_table), lcname, function_name-&stant.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);
return1; /* Dynamic */
}
efree(function_name-&stant.value.str.val);
function_name-&stant.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);
return0;
有⼏点需要理解的:
1,zend_resolve_non_class_name。由于php⽀持命名空间、也⽀持别名/导⼊等特性,因此⾸先要做的是将函数名称进⾏修正,否则在CG(function_table)中不到。例如,函数处于⼀个命名空间中,则可能需要将函数名添加上命名空间作为前缀,最终形成完整的函数名,也就是我们前⽂提到的以⼀种类似“全路径”的fullname作为函数名。再例如,函数名只是⼀个设置的别名,它实际指向了另⼀个命名空间中的某个函数,则需要将其改写成真正被调⽤函数的名称。这些⼯作,均由zend_resolve_non_class_name完成。命名空间添加了不少复杂度,下⾯是⼀些简单的例⼦:
<?php
namespace MyProject;
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
foo($bar); // zend_resolve_non_class_name会将foo处理成MyProject\foo
namespace\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我们下⾯再具体描述。
<?php
namespace MyProject\sub;
function foo($arg1)
{
print($arg1);
}
namespace MyProject;
$bar = 'hello php';
sub\foo($bar); // 以sub\foo调⽤函数,并不算shortname,因为存在\
注意上述例⼦,我们以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允许我们先调⽤,再去定义函数。例如:
<?php
$bar = 'hello php';
// 先调⽤
foo($bar);
// 后定义
function foo($arg1)
{
print($arg1);
}
4,在zend_do_begin_function_call的最后,我们将函数压⼊CG(function_call_stack)。这是⼀个栈,因为在后续对传参的编译,我们仍然需要⽤到函数,所以这⾥将其压亚⼊栈中,⽅便后⾯获取使⽤。之所以⽤栈,是因为调⽤函数传递的参数,可能是另⼀次函数调⽤。为了确保参数总是能到对应的函数,所以⽤栈。
<?php
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
foo(strlen($bar)); // ⾸先foo⼊栈,然后分析参数strlen($bar),发现依然是个函数,于是strlen⼊栈,再分析参数$bar,此时弹出对应的函数正好为strlen。
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:
<?php
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
$func = 'foo';
$func($bar); // 我们以变量$func作为函数名,试图调⽤函数foo,$func类型是IS_CV
此时,$func($bar) 对应function_call语法推导式的最后⼀条:
function_call:
.
..
| 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:
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
$func = 'foo';
$ref_func = 'func';
$$ref_func($bar); // 以可变变量的形式来调⽤函数,$$ref_func类型是IS_VAR
该例是以可变变量来调⽤函数,和例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的代码:
void zend_do_begin_dynamic_function_call(znode *function_name, int ns_call TSRMLS_DC) /* {{{ */
{
unsigned char *ptr = NULL;
zend_op *opline, *opline2;
// 拿⼀条zend_op
opline = get_next_op(CG(active_op_array) TSRMLS_CC);
// 参数ns_call表名是否以shortname在命名空间中调⽤函数
if (ns_call) {
char *slash;
int prefix_len, name_len;
/* In run-time PHP will check for function with full name and
internal function with short name */
// 第⼀条指令是ZEND_INIT_NS_FCALL_BY_NAME
opline->opcode = ZEND_INIT_NS_FCALL_BY_NAME;
opline->op2 = *function_name;
opline->extended_value = 0;
php延时函数opline->op1.op_type = IS_CONST;
Z_TYPE(opline->stant) = IS_STRING;
Z_STRVAL(opline->stant) = zend_str_tolower_dup(Z_STRVAL(opline->stant), Z_STRLEN(opline->stant));
Z_STRLEN(opline->stant) = Z_STRLEN(opline->stant);
opline->extended_value = zend_hash_func(Z_STRVAL(opline->stant), Z_STRLEN(opline->stant) + 1);
// 再拿⼀条zend_op,指令为ZEND_OP_DATA
slash = zend_memrchr(Z_STRVAL(opline->stant), '\\', Z_STRLEN(opline->stant));
prefix_len = slash-Z_STRVAL(opline->stant)+1;
name_len = Z_STRLEN(opline->stant)-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->stant) = IS_LONG;
if(!slash) {
zend_error(E_CORE_ERROR, "Namespaced name %s should contain slash", Z_STRVAL(opline->stant));
}
/* this is the length of namespace prefix */
Z_LVAL(opline2->stant) = 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->stant) = IS_STRING;
Z_STRVAL(opline->stant) = zend_str_tolower_dup(Z_STRVAL(opline->stant), Z_STRLEN(opline->stant));
Z_STRLEN(opline->stant) = Z_STRLEN(opline->stant);
opline->extended_value = zend_hash_func(Z_STRVAL(opline->stant), Z_STRLEN(opline->stant) + 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为例:
<?php
namespace MyProject;
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
foo($bar);
⽣成的op指令如下所⽰:
以ns_call = 0,先调⽤再定义为例:
<?php
$bar = 'hello php';
foo($bar);
function foo($arg1)
{
print($arg1);
}
⽣成的op指令如下所⽰:
以ns_call = 0,变量作为函数名为例:
<?php
function foo($arg1)
{
print($arg1);
}
$bar = 'hello php';
$func = 'foo';
$func($bar);
⽣成的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必定是函数体内的第⼀条指令。
如下图所⽰:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论