首页  >  文章  >  后端开发  >  函数的编译

函数的编译

WBOY
WBOY原创
2016-06-13 12:29:331422浏览

深入剖析php执行原理(2):函数的编译

本文只探讨纯粹的函数,并不包含方法。对于方法,会放到类、对象中一起研究。

想讲清楚在zend vm中,函数如何被正确的编译成op指令、如何发生参数传递、如何模拟调用栈、如何切换作用域等等,的确是一个很大范畴的话题。但为了弄明白php的原理,必须要攻克它。

对函数的研究,大致可以分成两块。第一块是函数体的编译,主要涉及到如何将函数转化成zend_op指令。第二块是研究函数的调用,涉及到函数调用语句的编译,以及函数如何被执行等topic。这里先来看看函数如何被编译,我们下一篇再讲函数的调用。

函数的编译

对函数进行编译,最终目的是为了生成一份对应的op指令集,除了op指令集,编译函数还会产生其他一些相关的数据,比如说函数名称、参数列表信息、compiled variables,甚至函数所在文件,起始行数等等。这些信息作为编译的产出,都需要保存起来。保存这些产出的数据结构,正是上一节中所描述的zend_op_array。下文会以op_array作为简称。

下面列出了一个简单的例子:

<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>);

这段代码包含了一个最简单的函数示例。

在这样一份php脚本中,最终其实会产生两个op_array。一个是由函数foo编译而来,另一个则是由除去foo之外代码编译生成的。同理可以推出,假如一份php脚本其中包含有2个函数和若干语句,则最终会产生3个op_array。也就是说,每个函数最终都会被编译成一个对应的op_array。

刚才提到,op_array中有一些字段是和函数息息相关的。比如function_name代表着函数的名称,比如num_args代表了函数的参数个数,比如required_num_args代表了必须的参数个数,比如arg_info代表着函数的参数信息...etc。

下面会继续结合这段代码,来研究foo函数详细的编译过程。

1、语法定义

从zend_language_parser.y文件中可以看出,函数的语法分析大致涉及如下几个推导式:

top_statement:<br>        statement                          { zend_verify_namespace(TSRMLS_C); }<br>    |    function_declaration_statement    { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br>    |    class_declaration_statement       { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br>    ...<br><br>function_declaration_statement:	unticked_function_declaration_statement	{ DO_TICKS(); };unticked_function_declaration_statement:	function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }	'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); };<br><br>is_reference:<br>        /* empty */    { $$.op_type = ZEND_RETURN_VAL; }<br>    |    '&'            { $$.op_type = ZEND_RETURN_REF; }<br>;<br><br>parameter_list:<br>        non_empty_parameter_list<br>    |    /* empty */<br>;non_empty_parameter_list:		optional_class_type T_VARIABLE				{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }	|	optional_class_type '&' T_VARIABLE			{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }	|	optional_class_type '&' T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }	|	optional_class_type T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }	|	non_empty_parameter_list ',' optional_class_type T_VARIABLE              { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$4, 0 TSRMLS_CC); }	|	non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE            { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$5, 1 TSRMLS_CC); }	|	non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE	 '=' static_scalar { znode tmp;  fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$7, &$3, &$5, 1 TSRMLS_CC); }	|	non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar    { znode tmp;  fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$6, &$3, &$4, 0 TSRMLS_CC); };

这里并没有截取完整,主要是缺少函数体内语句的语法分析,但已经足够我们弄清楚编译过程中的一些细节。

函数体内的语句,其对应的语法为inner_statement_list。inner_statement_list和函数体之外一般的语句并无二致,可以简单当成普通的语句来编译。

最重要的是看下unticked_function_declaration_statement,它定义了函数语法的骨架,同时还可以看出,函数编译中会执行zend_do_begin_function_declaration以及zend_do_end_function_declaration。这两步分别对应着下文提到的开始编译和结束编译。我们先来看zend_do_begin_function_declaration。

2、开始编译

当解析器遇到一段函数声明时,会尝试开始编译函数,这是通过执行zend_do_begin_function_declaration来完成的。

有两点:

1,函数是否返回引用,通过is_reference判断。可以看到在对is_reference进行语法分析时,可能会将op_type赋予ZEND_RETURN_VAL或ZEND_RETURN_REF。根据我们文章开始给出的php代码示例,函数foo并不返回引用,因此这里$2.op_type为ZEND_RETURN_VAL。话说由 function & func_name(){ ... } 这种形式来决定是否返回引用,已经很古老了,还是在CI框架中见过,现在很少有类似需求。

2,zend_do_begin_function_declaration接受的第一个参数,是对function字面进行词法分析生成的znode。这个znode被使用得非常巧妙,因为在编译函数时,zend vm必须将CG(active_op_array)切换成函数自己的op_array,以便于存储函数的编译结果,当函数编译完成之后,zend vm又需要将将CG(active_op_array)恢复成函数体外层的op_array。利用该znode保存函数体外的op_array,可以很方便的在函数编译结束时进行CG(active_op_array)恢复,具体后面会讲到。

研究下zend_do_begin_function_declaration的实现,比较长,我们分段来看:

<span style="color: #008000;">//</span><span style="color: #008000;"> 声明函数会变编译成的op_array</span><span style="color: #000000;">zend_op_array op_array;</span><span style="color: #008000;">//</span><span style="color: #008000;"> 函数名、长度、起始行数</span><span style="color: #0000ff;">char</span> *name = function_name-><span style="color: #000000;">u.constant.value.str.val;</span><span style="color: #0000ff;">int</span> name_len = function_name-><span style="color: #000000;">u.constant.value.str.len;</span><span style="color: #0000ff;">int</span> function_begin_line = function_token-><span style="color: #000000;">u.opline_num;zend_uint fn_flags;</span><span style="color: #0000ff;">char</span> *<span style="color: #000000;">lcname;zend_bool orig_interactive;ALLOCA_FLAG(use_heap)</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (is_method) {    ...} </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {    fn_flags </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 对函数来说,fn_flags没用,对方法来说,fn_flags指定了方法的修饰符</span><span style="color: #0000ff;">if</span> ((fn_flags & ZEND_ACC_STATIC) && (fn_flags & ZEND_ACC_ABSTRACT) && !(CG(active_class_entry)->ce_flags &<span style="color: #000000;"> ZEND_ACC_INTERFACE)) {    zend_error(E_STRICT, </span><span style="color: #800000;">"</span><span style="color: #800000;">Static function %s%s%s() should not be abstract</span><span style="color: #800000;">"</span>, is_method ? CG(active_class_entry)->name : <span style="color: #800000;">""</span>, is_method ? <span style="color: #800000;">"</span><span style="color: #800000;">::</span><span style="color: #800000;">"</span> : <span style="color: #800000;">""</span>, Z_STRVAL(function_name-><span style="color: #000000;">u.constant));}</span>

这段代码一开始就印证了我们先前的说法,每个函数都有一份自己的op_array。所以会在开头先声明一个op_array变量。

<span style="color: #008000;">//</span><span style="color: #008000;"> 第一个znode参数的妙处,它记录了当前的CG(active_op_array)</span>function_token->u.op_array =<span style="color: #000000;"> CG(active_op_array);lcname </span>=<span style="color: #000000;"> zend_str_tolower_dup(name, name_len);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 对op_array进行初始化,强制op_array.fn_flags会被初始化为0</span>orig_interactive =<span style="color: #000000;"> CG(interactive);CG(interactive) </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;init_op_array(</span>&<span style="color: #000000;">op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);CG(interactive) </span>=<span style="color: #000000;"> orig_interactive;</span><span style="color: #008000;">//</span><span style="color: #008000;"> 对op_array的一些设置</span>op_array.function_name =<span style="color: #000000;"> name;op_array.return_reference </span>=<span style="color: #000000;"> return_reference;op_array.fn_flags </span>|=<span style="color: #000000;"> fn_flags;op_array.pass_rest_by_reference </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;op_array.scope </span>= is_method ?<span style="color: #000000;"> CG(active_class_entry):NULL;op_array.prototype </span>=<span style="color: #000000;"> NULL;op_array.line_start </span>= 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 =<span style="color: #000000;"> get_next_op(CG(active_op_array) TSRMLS_CC);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果处于命名空间,则函数名还需要加上命名空间</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (CG(current_namespace)) {    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> Prefix function name with current namespace name </span><span style="color: #008000;">*/</span><span style="color: #000000;">    znode tmp;    tmp.u.constant </span>= *<span style="color: #000000;">CG(current_namespace);    zval_copy_ctor(</span>&<span style="color: #000000;">tmp.u.constant);    zend_do_build_namespace_name(</span>&tmp, &<span style="color: #000000;">tmp, function_name TSRMLS_CC);    op_array.function_name </span>=<span style="color: #000000;"> Z_STRVAL(tmp.u.constant);    efree(lcname);    name_len </span>=<span style="color: #000000;"> Z_STRLEN(tmp.u.constant);    lcname </span>=<span style="color: #000000;"> zend_str_tolower_dup(Z_STRVAL(tmp.u.constant), name_len);}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置opline</span>opline->opcode =<span style="color: #000000;"> ZEND_DECLARE_FUNCTION;</span><span style="color: #008000;">//</span><span style="color: #008000;"> 第一个操作数</span>opline->op1.op_type =<span style="color: #000000;"> IS_CONST;build_runtime_defined_function_key(</span>&opline-><span style="color: #000000;">op1.u.constant, lcname, name_len TSRMLS_CC);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 第二个操作数</span>opline->op2.op_type =<span style="color: #000000;"> IS_CONST;opline</span>->op2.u.constant.type =<span style="color: #000000;"> IS_STRING;opline</span>->op2.u.constant.value.str.val =<span style="color: #000000;"> lcname;opline</span>->op2.u.constant.value.str.len =<span style="color: #000000;"> name_len;Z_SET_REFCOUNT(opline</span>->op2.u.constant, <span style="color: #800080;">1</span><span style="color: #000000;">);opline</span>->extended_value =<span style="color: #000000;"> ZEND_DECLARE_FUNCTION;</span><span style="color: #008000;">//</span><span style="color: #008000;"> 切换CG(active_op_array)成函数自己的op_array</span>zend_hash_update(CG(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, <span style="color: #0000ff;">sizeof</span>(zend_op_array), (<span style="color: #0000ff;">void</span> **) &CG(active_op_array));

上面这段代码很关键。有几点要说明的:

1,如果函数是处于命名空间中,则其名称会被扩展成命名空间\函数名。比如:

<span style="color: #000000;">phpnamespace MyProject;</span><span style="color: #0000ff;">function</span> foo(<span style="color: #800080;">$arg1</span>, <span style="color: #800080;">$arg2</span> = 100<span style="color: #000000;">){    </span><span style="color: #0000ff;">print</span>(<span style="color: #800080;">$arg1</span><span style="color: #000000;">);}</span>

则会将函数名改为MyProject\foo。扩展工作由zend_do_build_namespace_name来完成。

2,build_runtime_defined_function_key会生成一个“key”。除了用到函数名称之外,还用到了函数所在文件路径、代码在内存中的地址等等。具体的实现可以自行阅读。将函数放进CG(function_table)时,用的键便是这个“key”。

3,代码中的op_line获取时,尚未发生CG(active_op_array)的切换。也就是说,op_line依然是外层op_array的一条指令。该指令具体为ZEND_DECLARE_FUNCTION,有两个操作数,第一个操作数保存了第二点中提到的“key”,第二个操作数则保存了形如"myproject\foo"这样的函数名(小写)。

4,这段代码的最后,将函数自身对应的op_array存放进了CG(function_table),同时,完成了CG(active_op_array)的切换。从这条语句开始,CG(active_op_array)便开始指向函数自己的op_array,而不再是函数体外层的op_array了。

继续来看zend_do_begin_function_declaration的最后一段:

<span style="color: #008000;">//</span><span style="color: #008000;"> 需要debuginfo,则函数体内的第一条zend_op,为ZEND_EXT_NOP</span><span style="color: #0000ff;">if</span> (CG(compiler_options) &<span style="color: #000000;"> ZEND_COMPILE_EXTENDED_INFO) {    zend_op </span>*opline =<span style="color: #000000;"> get_next_op(CG(active_op_array) TSRMLS_CC);    opline</span>->opcode =<span style="color: #000000;"> ZEND_EXT_NOP;    opline</span>->lineno =<span style="color: #000000;"> function_begin_line;    SET_UNUSED(opline</span>-><span style="color: #000000;">op1);    SET_UNUSED(opline</span>-><span style="color: #000000;">op2);}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 控制switch和foreach内声明的函数</span><span style="color: #000000;">{    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> Push a seperator to the switch and foreach stacks </span><span style="color: #008000;">*/</span><span style="color: #000000;">    zend_switch_entry switch_entry;    switch_entry.cond.op_type </span>=<span style="color: #000000;"> IS_UNUSED;    switch_entry.default_case </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;    switch_entry.control_var </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;    zend_stack_push(</span>&CG(switch_cond_stack), (<span style="color: #0000ff;">void</span> *) &switch_entry, <span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(switch_entry));    {        </span><span style="color: #008000;">/*</span><span style="color: #008000;"> Foreach stack separator </span><span style="color: #008000;">*/</span><span style="color: #000000;">        zend_op dummy_opline;        dummy_opline.result.op_type </span>=<span style="color: #000000;"> IS_UNUSED;        dummy_opline.op1.op_type </span>=<span style="color: #000000;"> IS_UNUSED;        zend_stack_push(</span>&, (<span style="color: #0000ff;">void</span> *) &dummy_opline, <span style="color: #0000ff;">sizeof</span><span style="color: #000000;">(zend_op));    }}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 保存函数的注释语句</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (CG(doc_comment)) {    CG(active_op_array)</span>->doc_comment =<span style="color: #000000;"> CG(doc_comment);    CG(active_op_array)</span>->doc_comment_len =<span style="color: #000000;"> CG(doc_comment_len);    CG(doc_comment) </span>=<span style="color: #000000;"> NULL;    CG(doc_comment_len) </span>= <span style="color: #800080;">0</span><span style="color: #000000;">;}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 作用和上面switch,foreach是一样的,函数体内的语句并不属于函数体外的label</span>zend_stack_push(&CG(labels_stack), (<span style="color: #0000ff;">void</span> *) &CG(labels), <span style="color: #0000ff;">sizeof</span>(HashTable*<span style="color: #000000;">));CG(labels) </span>= 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。举例来说,

<span style="color: #000000;">phplabel1</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;">);    goto label2;    label2</span>:    <span style="color: #0000ff;">exit</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>);

解释器在进入zend_do_begin_function_declaration时,CG(labels)中保存的是“label1”。当解释器开始编译函数foo,则需要将“label1”保存到CG(labels_stack)中,同时清空CG(labels)。因为在编译foo的过程中,CG(labels)会保存“labe2”。当foo编译完成,会利用CG(labels_stack)来恢复CG(labels),则CG(labels)再次变成“label1”。

至此,整个zend_do_begin_function_declaration过程已经全部分析完成。最重要的是,一旦完成zend_do_begin_function_declaration,CG(active_op_array)就指向了函数自身对应的op_array。同时,也利用生成的“key”在CG(function_table)中替函数占了一个位

3、编译参数列表

函数可以定义为不接受任何参数,对于参数列表为空的情况,其实不做任何处理。我们前文的例子foo函数,接受了一个参数$arg1,我们下面还是分析有参数的情况。

根据语法推导式non_empty_parameter_list的定义,参数列表一共有8种,前4种对应的是一个参数,后4种对应多个参数。我们只关心前4种,后4种编译的过程,仅仅是重复前4种的步骤而已。

optional_class_type T_VARIABLE				{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); }optional_class_type '&' T_VARIABLE			{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); }optional_class_type '&' T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); }optional_class_type T_VARIABLE '=' static_scalar	{ znode tmp;  fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }

前4种情况,具体又可以分为2类,1类没有默认值,区别只在于参数的传递是否采用引用,而另1类,都有默认值“static_scalar”。

实际上区别并不大,它们的语法分析的处理过程也几乎一致。都是先调用fetch_simple_variable,再执行zend_do_receive_arg。有没有默认值,区别也仅仅在于zend_do_receive_arg的参数,会不会将默认值传递进去。先来看fetch_simple_variable。

3.1 fetch_simple_variable

fetch_simple_variable是用来获取compiled variables索引的。compiled variables被视作php的性能提升手段之一,因为它利用数组存储了变量,而并非内核中普遍使用的HashTable。这里可以看出,函数的任何一个参数,均会被编译为compiled variables,compiled variables被保存在函数体op_array->vars数组中。虽然根据变量名称去HashTable查询,效率并不低。但显然根据索引去op_array->vars数组中获取变量,会更加高效。

<span style="color: #0000ff;">void</span> fetch_simple_variable_ex(znode *result, znode *varname, <span style="color: #0000ff;">int</span> bp, zend_uchar op TSRMLS_DC) <span style="color: #008000;">/*</span><span style="color: #008000;"> {{{ </span><span style="color: #008000;">*/</span><span style="color: #000000;">{    zend_op opline;    ...    </span><span style="color: #0000ff;">if</span> (varname->op_type ==<span style="color: #000000;"> IS_CONST) {        </span><span style="color: #0000ff;">if</span> (Z_TYPE(varname->u.constant) !=<span style="color: #000000;"> IS_STRING) {            convert_to_string(</span>&varname-><span style="color: #000000;">u.constant);        }        </span><span style="color: #0000ff;">if</span> (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len TSRMLS_CC) &&            !(varname->u.constant.value.str.len == (<span style="color: #0000ff;">sizeof</span>(<span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>)-<span style="color: #800080;">1</span>) && !memcmp(varname->u.constant.value.str.val, <span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">sizeof</span>(<span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>))) &&<span style="color: #000000;">            (CG(active_op_array)</span>->last == <span style="color: #800080;">0</span> || CG(active_op_array)->opcodes[CG(active_op_array)->last-<span style="color: #800080;">1</span>].opcode !=<span style="color: #000000;"> ZEND_BEGIN_SILENCE)) {                        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 节点的类型为IS_CV,表明是compiled variables</span>            result->op_type =<span style="color: #000000;"> IS_CV;                        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 用u.var来记录compiled variables在CG(active_op_array)->vars中的索引</span>            result->u.<span style="color: #0000ff;">var</span> = lookup_cv(CG(active_op_array), varname->u.constant.value.str.val, varname-><span style="color: #000000;">u.constant.value.str.len);            result</span>->u.EA.type = <span style="color: #800080;">0</span><span style="color: #000000;">;            varname</span>->u.constant.value.str.val = CG(active_op_array)->vars[result->u.<span style="color: #0000ff;">var</span><span style="color: #000000;">].name;            </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;        }    }    ...}</span>

这里不做详细的分析了。当fetch_simple_variable获取索引之后,znode中就不必再保存变量的名称,取而代之的是变量在vars数组中的索引,即znode->u.var,其类型为int。fetch_simple_variable完成,会进入zend_do_receive_arg。

3.2 zend_do_receive_arg

zend_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 *<span style="color: #000000;">opline;zend_arg_info </span>*<span style="color: #000000;">cur_arg_info;</span><span style="color: #008000;">//</span><span style="color: #008000;"> class_type主要用于限制函数参数的类型</span><span style="color: #0000ff;">if</span> (class_type->op_type == IS_CONST && Z_TYPE(class_type->u.constant) == IS_STRING && Z_STRLEN(class_type->u.constant) == <span style="color: #800080;">0</span><span style="color: #000000;">) {    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> Usage of namespace as class name not in namespace </span><span style="color: #008000;">*/</span><span style="color: #000000;">    zval_dtor(</span>&class_type-><span style="color: #000000;">u.constant);    zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">Cannot use 'namespace' as a class name</span><span style="color: #800000;">"</span><span style="color: #000000;">);    </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;}</span><span style="color: #008000;">//</span><span style="color: #008000;"> 对静态方法来说,参数不能为this</span><span style="color: #0000ff;">if</span> (<span style="color: #0000ff;">var</span>->op_type == IS_CV && <span style="color: #0000ff;">var</span>->u.<span style="color: #0000ff;">var</span> == CG(active_op_array)->this_var && (CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span style="color: #800080;">0</span><span style="color: #000000;">) {    zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">Cannot re-assign $this</span><span style="color: #800000;">"</span><span style="color: #000000;">);} </span><span style="color: #0000ff;">else</span> <span style="color: #0000ff;">if</span> (<span style="color: #0000ff;">var</span>->op_type == IS_VAR && CG(active_op_array)->scope && ((CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span style="color: #800080;">0</span>) && (Z_TYPE(varname->u.constant) == IS_STRING) && (Z_STRLEN(varname->u.constant) == <span style="color: #0000ff;">sizeof</span>(<span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>)-<span style="color: #800080;">1</span>) && (memcmp(Z_STRVAL(varname->u.constant), <span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>, <span style="color: #0000ff;">sizeof</span>(<span style="color: #800000;">"</span><span style="color: #800000;">this</span><span style="color: #800000;">"</span>)) == <span style="color: #800080;">0</span><span style="color: #000000;">)) {    zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">Cannot re-assign $this</span><span style="color: #800000;">"</span><span style="color: #000000;">);}</span><span style="color: #008000;">//</span><span style="color: #008000;"> CG(active_op_array)此时已经是函数体的op_array了,这里拿一条指令</span>opline =<span style="color: #000000;"> get_next_op(CG(active_op_array) TSRMLS_CC);CG(active_op_array)</span>->num_args++<span style="color: #000000;">;opline</span>->opcode =<span style="color: #000000;"> op;opline</span>->result = *<span style="color: #0000ff;">var</span><span style="color: #000000;">;</span><span style="color: #008000;">//</span><span style="color: #008000;"> op1节点表明是第几个参数</span>opline->op1 = *<span style="color: #000000;">offset;</span><span style="color: #008000;">//</span><span style="color: #008000;"> op2节点可能为初始值,也可能为UNUSED</span><span style="color: #0000ff;">if</span> (op ==<span style="color: #000000;"> ZEND_RECV_INIT) {    opline</span>->op2 = *<span style="color: #000000;">initialization;} </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {    CG(active_op_array)</span>->required_num_args = CG(active_op_array)-><span style="color: #000000;">num_args;    SET_UNUSED(opline</span>-><span style="color: #000000;">op2);}</span>

上面这段代码,首先通过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。

继续来看:

<span style="color: #008000;">//</span><span style="color: #008000;"> 这里采用erealloc进行分配,因为期望最终会形成一个参数信息的数组</span>CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info, <span style="color: #0000ff;">sizeof</span>(zend_arg_info)*(CG(active_op_array)-><span style="color: #000000;">num_args));</span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置当前的zend_arg_info</span>cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-<span style="color: #800080;">1</span><span style="color: #000000;">];cur_arg_info</span>->name = estrndup(varname->u.constant.value.str.val, varname-><span style="color: #000000;">u.constant.value.str.len);cur_arg_info</span>->name_len = varname-><span style="color: #000000;">u.constant.value.str.len;cur_arg_info</span>->array_type_hint = <span style="color: #800080;">0</span><span style="color: #000000;">;cur_arg_info</span>->allow_null = <span style="color: #800080;">1</span><span style="color: #000000;">;cur_arg_info</span>->pass_by_reference =<span style="color: #000000;"> pass_by_reference;cur_arg_info</span>->class_name =<span style="color: #000000;"> NULL;cur_arg_info</span>->class_name_len = <span style="color: #800080;">0</span><span style="color: #000000;">;</span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果需要对参数做类型限定</span><span style="color: #0000ff;">if</span> (class_type->op_type !=<span style="color: #000000;"> IS_UNUSED) {    cur_arg_info</span>->allow_null = <span style="color: #800080;">0</span><span style="color: #000000;">;        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 限定为类</span>    <span style="color: #0000ff;">if</span> (class_type->u.constant.type ==<span style="color: #000000;"> IS_STRING) {        </span><span style="color: #0000ff;">if</span> (ZEND_FETCH_CLASS_DEFAULT == zend_get_class_fetch_type(Z_STRVAL(class_type->u.constant), Z_STRLEN(class_type-><span style="color: #000000;">u.constant))) {            zend_resolve_class_name(class_type, </span>&opline->extended_value, <span style="color: #800080;">1</span><span style="color: #000000;"> TSRMLS_CC);        }        cur_arg_info</span>->class_name = class_type-><span style="color: #000000;">u.constant.value.str.val;        cur_arg_info</span>->class_name_len = class_type-><span style="color: #000000;">u.constant.value.str.len;                </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果限定为类,则参数的默认值只能为NULL</span>        <span style="color: #0000ff;">if</span> (op ==<span style="color: #000000;"> ZEND_RECV_INIT) {            </span><span style="color: #0000ff;">if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span style="color: #800000;">"</span><span style="color: #800000;">NULL</span><span style="color: #800000;">"</span><span style="color: #000000;">))) {                cur_arg_info</span>->allow_null = <span style="color: #800080;">1</span><span style="color: #000000;">;            } </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {                zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">Default value for parameters with a class type hint can only be NULL</span><span style="color: #800000;">"</span><span style="color: #000000;">);            }        }    }    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 限定为数组</span>    <span style="color: #0000ff;">else</span><span style="color: #000000;"> {        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 将array_type_hint设置为1</span>        cur_arg_info->array_type_hint = <span style="color: #800080;">1</span><span style="color: #000000;">;        cur_arg_info</span>->class_name =<span style="color: #000000;"> NULL;        cur_arg_info</span>->class_name_len = <span style="color: #800080;">0</span><span style="color: #000000;">;                </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果限定为数组,则参数的默认值只能为数组或NULL</span>        <span style="color: #0000ff;">if</span> (op ==<span style="color: #000000;"> ZEND_RECV_INIT) {            </span><span style="color: #0000ff;">if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span style="color: #800000;">"</span><span style="color: #800000;">NULL</span><span style="color: #800000;">"</span><span style="color: #000000;">))) {                cur_arg_info</span>->allow_null = <span style="color: #800080;">1</span><span style="color: #000000;">;            } </span><span style="color: #0000ff;">else</span> <span style="color: #0000ff;">if</span> (Z_TYPE(initialization->u.constant) != IS_ARRAY && Z_TYPE(initialization->u.constant) !=<span style="color: #000000;"> IS_CONSTANT_ARRAY) {                zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">Default value for parameters with array type hint can only be an array or NULL</span><span style="color: #800000;">"</span><span style="color: #000000;">);            }        }    }}opline</span>->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函数会修正类名。举例来说:

<span style="color: #000000;">phpnamespace A;</span><span style="color: #0000ff;">class</span><span style="color: #000000;"> B { }</span><span style="color: #0000ff;">function</span> foo(B <span style="color: #800080;">$arg1</span>, <span style="color: #800080;">$arg2</span> = 100<span style="color: #000000;">){    </span><span style="color: #0000ff;">print</span>(<span style="color: #800080;">$arg1</span><span style="color: #000000;">);}</span>

我们期望参数arg1的类型为B,class_type中也保存了B。但是因为位于命名空间A下,所以,zend_resolve_class_name会将class_type中保存的类名B,修正为A\B。

OK,到这里,zend_do_receive_arg已经全部分析完。zend vm在分析函数参数时,每遇见一个参数,便会调用一次zend_do_receive_arg,生成一条RECV指令。因此,函数有几个参数,就会编译出几条RECV指令。

4、编译函数体

当编译完参数列表,zend vm便会进入函数内部了。函数体的编译其实和正常语句的编译一样。zend vm只需要将函数体内部的php语句,按照正常的statment,进行词法分析、语法分析来处理,最终形成一条条zend_op指令。

来看下语法文件:

unticked_function_declaration_statement:	function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }	'(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); }<br>;

函数体内部的语句,表示为inner_statement_list。

inner_statement_list:		inner_statement_list  { zend_do_extended_info(TSRMLS_C); } inner_statement { HANDLE_INTERACTIVE(); }	|	/* empty */;

而inner_statment正是由语句、函数声明、类声明组成的。

inner_statement:		statement	|	function_declaration_statement	|	class_declaration_statement	|	T_HALT_COMPILER '(' ')' ';'   { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); };

inner_statement并非专门用于函数,其他譬如foreach,while循环等有block语句块中,都会被识别为inner_statement。从这里其实还能看到一些有意思的语法,比如说我们可以在函数里声明一个类。inner_statement就不展开叙述了,否则相当于将整个php的语法捋一遍,情况太多了。

5、结束编译

我们最后来看下结束编译的过程。结束函数编译是通过zend_do_end_function_declaration来完成的。

zend_do_end_function_declaration接收的参数function_token,其实就是前面提到过的function字面对应的znode。根据我们在“开始编译”一节所述,function_token中保留了函数体之外的op_array。

<span style="color: #0000ff;">char</span> lcname[<span style="color: #800080;">16</span><span style="color: #000000;">];</span><span style="color: #0000ff;">int</span><span style="color: #000000;"> name_len;zend_do_extended_info(TSRMLS_C);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 返回NULL</span>zend_do_return(NULL, <span style="color: #800080;">0</span><span style="color: #000000;"> TSRMLS_CC);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 通过op指令设置对应的handler函数</span><span style="color: #000000;">pass_two(CG(active_op_array) TSRMLS_CC);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 释放当前函数的CG(labels),并从CG(labels_stack)中还原之前的CG(labels)</span><span style="color: #000000;">zend_release_labels(TSRMLS_C);</span><span style="color: #0000ff;">if</span><span style="color: #000000;"> (CG(active_class_entry)) {    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 检查魔术方法的参数是否合法</span>    zend_check_magic_method_implementation(CG(active_class_entry), (zend_function*<span style="color: #000000;">)CG(active_op_array), E_COMPILE_ERROR TSRMLS_CC);} </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> we don't care if the function name is longer, in fact lowercasing only      * the beginning of the name speeds up the check process </span><span style="color: #008000;">*/</span><span style="color: #000000;">    name_len </span>= strlen(CG(active_op_array)-><span style="color: #000000;">function_name);    zend_str_tolower_copy(lcname, CG(active_op_array)</span>->function_name, MIN(name_len, <span style="color: #0000ff;">sizeof</span>(lcname)-<span style="color: #800080;">1</span><span style="color: #000000;">));    lcname[</span><span style="color: #0000ff;">sizeof</span>(lcname)-<span style="color: #800080;">1</span>] = <span style="color: #800000;">'</span><span style="color: #800000;">\0</span><span style="color: #800000;">'</span>; <span style="color: #008000;">/*</span><span style="color: #008000;"> zend_str_tolower_copy won't necessarily set the zero byte </span><span style="color: #008000;">*/</span>        <span style="color: #008000;">//</span><span style="color: #008000;"> 检查__autoload函数的参数是否合法</span>    <span style="color: #0000ff;">if</span> (name_len == <span style="color: #0000ff;">sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME) - <span style="color: #800080;">1</span> && !memcmp(lcname, ZEND_AUTOLOAD_FUNC_NAME, <span style="color: #0000ff;">sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME)) && CG(active_op_array)->num_args != <span style="color: #800080;">1</span><span style="color: #000000;">) {        zend_error(E_COMPILE_ERROR, </span><span style="color: #800000;">"</span><span style="color: #800000;">%s() must take exactly 1 argument</span><span style="color: #800000;">"</span><span style="color: #000000;">, ZEND_AUTOLOAD_FUNC_NAME);    }        }CG(active_op_array)</span>->line_end =<span style="color: #000000;"> zend_get_compiled_lineno(TSRMLS_C);</span><span style="color: #008000;">//</span><span style="color: #008000;"> 很关键!将CG(active_op_array)还原成函数外层的op_array</span>CG(active_op_array) = function_token-><span style="color: #000000;">u.op_array;</span><span style="color: #008000;">/*</span><span style="color: #008000;"> Pop the switch and foreach seperators </span><span style="color: #008000;">*/</span><span style="color: #000000;">zend_stack_del_top(</span>&<span style="color: #000000;">CG(switch_cond_stack));zend_stack_del_top(</span>&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指令。举例来说:

<span style="color: #0000ff;">function</span> foo(<span style="color: #000000;">)<br><span style="color: #000000;">{</span>    </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;}</span>

编译出来的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)宏来完成的。

<span style="color: #0000ff;">#define</span> ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)

zend_vm_set_opcode_handler的实现很简单:

<span style="color: #0000ff;">void</span> zend_init_opcodes_handlers(<span style="color: #0000ff;">void</span><span style="color: #000000;">){    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 超大的数组,里面存放了所有的handler</span>    <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">const</span> opcode_handler_t labels[] =<span style="color: #000000;"> {        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 </span>= (opcode_handler_t*<span style="color: #000000;">)labels;}</span><span style="color: #0000ff;">static</span> opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op*<span style="color: #000000;"> op){        </span><span style="color: #0000ff;">static</span> <span style="color: #0000ff;">const</span> <span style="color: #0000ff;">int</span> zend_vm_decode[] =<span style="color: #000000;"> {            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 0              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _CONST_CODE,  </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 1 = IS_CONST   </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _TMP_CODE,    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 2 = IS_TMP_VAR </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 3              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _VAR_CODE,    </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 4 = IS_VAR     </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 5              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 6              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 7              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 8 = IS_UNUSED  </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 9              </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 10             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 11             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 12             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 13             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 14             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _UNUSED_CODE, </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 15             </span><span style="color: #008000;">*/</span><span style="color: #000000;">            _CV_CODE      </span><span style="color: #008000;">/*</span><span style="color: #008000;"> 16 = IS_CV     </span><span style="color: #008000;">*/</span><span style="color: #000000;">        };                </span><span style="color: #008000;">//</span><span style="color: #008000;"> 去handler数组里找到对应的处理函数</span>        <span style="color: #0000ff;">return</span> zend_opcode_handlers[opcode * <span style="color: #800080;">25</span> + zend_vm_decode[op->op1.op_type] * <span style="color: #800080;">5</span> + zend_vm_decode[op-><span style="color: #000000;">op2.op_type]];}ZEND_API </span><span style="color: #0000ff;">void</span> zend_vm_set_opcode_handler(zend_op*<span style="color: #000000;"> op){    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 给zend op设置对应的handler函数</span>    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op-><span style="color: #000000;">opcode], op);}</span>

所有的opcode都定义在zend_vm_opcodes.h里,从php5.3-php5.6,大概从150增长到170个opcode。上面可以看到通过opcode查找handler的准确算法:

zend_opcode_handlers[opcode * <span style="color: #800080;">25</span> + zend_vm_decode[op->op1.op_type] * <span style="color: #800080;">5</span> + 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:

<span style="color: #008000;">//</span><span style="color: #008000;"> 拿到的是最近一条zend op,对于函数来说,就是ZEND_DECLARE_FUNCTION</span>zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-<span style="color: #800080;">1</span><span style="color: #000000;">];HashTable </span>*<span style="color: #000000;">table;</span><span style="color: #0000ff;">while</span> (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)-><span style="color: #000000;">opcodes) {    opline</span>--<span style="color: #000000;">;}</span><span style="color: #0000ff;">switch</span> (opline-><span style="color: #000000;">opcode) {    </span><span style="color: #0000ff;">case</span><span style="color: #000000;"> ZEND_DECLARE_FUNCTION:        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 真正绑定函数</span>        <span style="color: #0000ff;">if</span> (do_bind_function(opline, CG(function_table), <span style="color: #800080;">1</span>) ==<span style="color: #000000;"> FAILURE) {            </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;        }        table </span>=<span style="color: #000000;"> CG(function_table);        </span><span style="color: #0000ff;">break</span><span style="color: #000000;">;    </span><span style="color: #0000ff;">case</span><span style="color: #000000;"> ZEND_DECLARE_CLASS:        ...    </span><span style="color: #0000ff;">case</span><span style="color: #000000;"> ZEND_DECLARE_INHERITED_CLASS:        ...}</span><span style="color: #008000;">//</span><span style="color: #008000;"> op1中保存的是函数的key,这里其从将CG(function_table)中删除</span>zend_hash_del(table, opline->op1.u.constant.value.str.val, opline-><span style="color: #000000;">op1.u.constant.value.str.len);zval_dtor(</span>&opline-><span style="color: #000000;">op1.u.constant);zval_dtor(</span>&opline-><span style="color: #000000;">op2.u.constant);</span><span style="color: #008000;">//</span><span style="color: #008000;"> opline置为NOP</span>MAKE_NOP(opline);

这个函数实现也很简单,主要就是调用了do_bind_function。

ZEND_API <span style="color: #0000ff;">int</span> do_bind_function(zend_op *opline, HashTable *function_table, zend_bool compile_time) <span style="color: #008000;">/*</span><span style="color: #008000;"> {{{ </span><span style="color: #008000;">*/</span><span style="color: #000000;">{    zend_function </span>*<span style="color: #000000;">function;    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 找出函数</span>    zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (<span style="color: #0000ff;">void</span> *) &<span style="color: #000000;">function);        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 以函数名称作为key,重新加入function_table</span>    <span style="color: #0000ff;">if</span> (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span style="color: #800080;">1</span>, function, <span style="color: #0000ff;">sizeof</span>(zend_function), NULL)==<span style="color: #000000;">FAILURE) {        </span><span style="color: #0000ff;">int</span> error_level = compile_time ?<span style="color: #000000;"> E_COMPILE_ERROR : E_ERROR;        zend_function </span>*<span style="color: #000000;">old_function;        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 加入失败,可能发生重复定义了</span>        <span style="color: #0000ff;">if</span> (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span style="color: #800080;">1</span>, (<span style="color: #0000ff;">void</span> *) &old_function)==<span style="color: #000000;">SUCCESS            </span>&& old_function->type ==<span style="color: #000000;"> ZEND_USER_FUNCTION            </span>&& old_function->op_array.last > <span style="color: #800080;">0</span><span style="color: #000000;">) {            zend_error(error_level, </span><span style="color: #800000;">"</span><span style="color: #800000;">Cannot redeclare %s() (previously declared in %s:%d)</span><span style="color: #800000;">"</span><span style="color: #000000;">,                        function</span>->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[<span style="color: #800080;">0</span><span style="color: #000000;">].lineno);        } </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {            zend_error(error_level, </span><span style="color: #800000;">"</span><span style="color: #800000;">Cannot redeclare %s()</span><span style="color: #800000;">"</span>, function-><span style="color: #000000;">common.function_name);        }        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> FAILURE;    } </span><span style="color: #0000ff;">else</span><span style="color: #000000;"> {        (</span>*function->op_array.refcount)++<span style="color: #000000;">;        function</span>->op_array.static_variables = NULL; <span style="color: #008000;">/*</span><span style="color: #008000;"> NULL out the unbound function </span><span style="color: #008000;">*/</span>        <span style="color: #0000ff;">return</span><span style="color: #000000;"> SUCCESS;    }}</span>

在进入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。一些关键的步骤如下图:

 

至于函数的调用,又是另外一个话题了。

 

 

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn