PHP実行原理の徹底分析(4):関数呼び出し、php_PHPの徹底分析チュートリアル

WBOY
リリース: 2016-07-12 08:50:28
オリジナル
921 人が閲覧しました

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

本章开始研究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(&$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);} ;

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_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); }
| 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); }
| 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); }
| 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); }
;
ログイン後にコピー

其结构并不复杂:

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->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完成。命名空间添加了不少复杂度,下面是一些简单的例子:

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; 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为例:

php namespace MyProject; function foo($arg1) { print($arg1); } $bar = 'hello php'; foo($bar);
ログイン後にコピー

生成的op指令如下所示:

PHP実行原理の徹底分析(4):関数呼び出し、php_PHPの徹底分析チュートリアル php$bar= 'hello php'; foo($bar);functionfoo($arg1) {print($arg1); }

生成的op指令如下所示:

PHP実行原理の徹底分析(4):関数呼び出し、php_PHPの徹底分析チュートリアル phpfunctionfoo($arg1) {print($arg1); }$bar= 'hello php';$func= 'foo';$func($bar);

生成的op指令如下所示:

PHP実行原理の徹底分析(4):関数呼び出し、php_PHPの徹底分析チュートリアルvoidzend_do_pass_param(znode *param, zend_uchar op,intoffset TSRMLS_DC)/*{{{*/{ zend_op*opline;intoriginal_op =op; zend_function**function_ptr_ptr, *function_ptr;intsend_by_reference;intsend_function =0;//从CG(function_call_stack)获取当前函数,注意可能拿出的是NULLzend_stack_top(&CG(function_call_stack), (void**) &function_ptr_ptr); function_ptr= *function_ptr_ptr;//调用的地方以引用传参,但是php.ini中配置不允许这样,则抛错if(original_op == ZEND_SEND_REF && !CG(allow_call_time_pass_reference)) {if(function_ptr &&function_ptr->common.function_name &&function_ptr->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置为0 else { send_by_reference = 0; } // 如果用户传递的参数,本身就是一次函数调用,则将op改成ZEND_SEND_VAR_NO_REF if (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_REF else 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_REF if (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_VARIS_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_VAR opline->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指令。

www.bkjia.com true http://www.bkjia.com/PHPjc/1133568.html TechArticle 深入剖析php执行原理(4):函数的调用,深入剖析php 本章开始研究php中函数的调用和执行,先来看函数调用语句是如何被编译的。 我们前...
関連ラベル:
php
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!