程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 深入剖析php執行原理(2):函數的編譯,深入剖析php

深入剖析php執行原理(2):函數的編譯,深入剖析php

編輯:關於PHP編程

深入剖析php執行原理(2):函數的編譯,深入剖析php


本文只探討純粹的函數,並不包含方法。對於方法,會放到類、對象中一起研究。

想講清楚在zend vm中,函數如何被正確的編譯成op指令、如何發生參數傳遞、如何模擬調用棧、如何切換作用域等等,的確是一個很大范疇的話題。但為了弄明白php的原理,必須要攻克它。

對函數的研究,大致可以分成兩塊。第一塊是函數體的編譯,主要涉及到如何將函數轉化成zend_op指令。第二塊是研究函數的調用,涉及到函數調用語句的編譯,以及函數如何被執行等topic。這裡先來看看函數如何被編譯,我們下一篇再講函數的調用。

函數的編譯

對函數進行編譯,最終目的是為了生成一份對應的op指令集,除了op指令集,編譯函數還會產生其他一些相關的數據,比如說函數名稱、參數列表信息、compiled variables,甚至函數所在文件,起始行數等等。這些信息作為編譯的產出,都需要保存起來。保存這些產出的數據結構,正是上一節中所描述的zend_op_array。下文會以op_array作為簡稱。

下面列出了一個簡單的例子:

<?php
function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
foo($bar);

這段代碼包含了一個最簡單的函數示例。

在這樣一份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:
        statement                         { zend_verify_namespace(TSRMLS_C); }
    |    function_declaration_statement    { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }
    |    class_declaration_statement       { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }
...

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

is_reference:
        /* empty */    { $$.op_type = ZEND_RETURN_VAL; }
    |    '&'            { $$.op_type = ZEND_RETURN_REF; }
;

parameter_list:
        non_empty_parameter_list
    |    /* empty */
; 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的實現,比較長,我們分段來看:

// 聲明函數會變編譯成的op_array
zend_op_array op_array;

// 函數名、長度、起始行數
char *name = function_name->u.constant.value.str.val;
int name_len = function_name->u.constant.value.str.len;
int function_begin_line = function_token->u.opline_num;
zend_uint fn_flags;
char *lcname;
zend_bool orig_interactive;
ALLOCA_FLAG(use_heap)

if (is_method) {
    ...
} else {
    fn_flags = 0;
}

// 對函數來說,fn_flags沒用,對方法來說,fn_flags指定了方法的修飾符
if ((fn_flags & ZEND_ACC_STATIC) && (fn_flags & ZEND_ACC_ABSTRACT) && !(CG(active_class_entry)->ce_flags & ZEND_ACC_INTERFACE)) {
    zend_error(E_STRICT, "Static function %s%s%s() should not be abstract", is_method ? CG(active_class_entry)->name : "", is_method ? "::" : "", Z_STRVAL(function_name->u.constant));
}

這段代碼一開始就印證了我們先前的說法,每個函數都有一份自己的op_array。所以會在開頭先聲明一個op_array變量。

// 第一個znode參數的妙處,它記錄了當前的CG(active_op_array)
function_token->u.op_array = CG(active_op_array);
lcname = zend_str_tolower_dup(name, name_len);

// 對op_array進行初始化,強制op_array.fn_flags會被初始化為0
orig_interactive = CG(interactive);
CG(interactive) = 0;
init_op_array(&op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);
CG(interactive) = orig_interactive;

// 對op_array的一些設置
op_array.function_name = name;
op_array.return_reference = return_reference;
op_array.fn_flags |= fn_flags;
op_array.pass_rest_by_reference = 0;
op_array.scope = is_method ? CG(active_class_entry):NULL;
op_array.prototype = NULL;
op_array.line_start = 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 = get_next_op(CG(active_op_array) TSRMLS_CC);

// 如果處於命名空間,則函數名還需要加上命名空間
if (CG(current_namespace)) {
    /* Prefix function name with current namespace name */
    znode tmp;

    tmp.u.constant = *CG(current_namespace);
    zval_copy_ctor(&tmp.u.constant);
    zend_do_build_namespace_name(&tmp, &tmp, function_name TSRMLS_CC);
    op_array.function_name = Z_STRVAL(tmp.u.constant);
    efree(lcname);
    name_len = Z_STRLEN(tmp.u.constant);
    lcname = zend_str_tolower_dup(Z_STRVAL(tmp.u.constant), name_len);
}

// 設置opline
opline->opcode = ZEND_DECLARE_FUNCTION;
// 第一個操作數
opline->op1.op_type = IS_CONST;
build_runtime_defined_function_key(&opline->op1.u.constant, lcname, name_len TSRMLS_CC);
// 第二個操作數
opline->op2.op_type = IS_CONST;
opline->op2.u.constant.type = IS_STRING;
opline->op2.u.constant.value.str.val = lcname;
opline->op2.u.constant.value.str.len = name_len;
Z_SET_REFCOUNT(opline->op2.u.constant, 1);
opline->extended_value = ZEND_DECLARE_FUNCTION;

// 切換CG(active_op_array)成函數自己的op_array
zend_hash_update(CG(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, sizeof(zend_op_array), (void **) &CG(active_op_array));

上面這段代碼很關鍵。有幾點要說明的:

1,如果函數是處於命名空間中,則其名稱會被擴展成命名空間\函數名。比如:

<?php
namespace MyProject;

function foo($arg1, $arg2 = 100)
{
    print($arg1);
}

則會將函數名改為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的最後一段:

// 需要debuginfo,則函數體內的第一條zend_op,為ZEND_EXT_NOP
if (CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) {
    zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

    opline->opcode = ZEND_EXT_NOP;
    opline->lineno = function_begin_line;
    SET_UNUSED(opline->op1);
    SET_UNUSED(opline->op2);
}

// 控制switch和foreach內聲明的函數
{
    /* Push a seperator to the switch and foreach stacks */
    zend_switch_entry switch_entry;

    switch_entry.cond.op_type = IS_UNUSED;
    switch_entry.default_case = 0;
    switch_entry.control_var = 0;

    zend_stack_push(&CG(switch_cond_stack), (void *) &switch_entry, sizeof(switch_entry));

    {
        /* Foreach stack separator */
        zend_op dummy_opline;

        dummy_opline.result.op_type = IS_UNUSED;
        dummy_opline.op1.op_type = IS_UNUSED;

        zend_stack_push(&, (void *) &dummy_opline, sizeof(zend_op));
    }
}

// 保存函數的注釋語句
if (CG(doc_comment)) {
    CG(active_op_array)->doc_comment = CG(doc_comment);
    CG(active_op_array)->doc_comment_len = CG(doc_comment_len);
    CG(doc_comment) = NULL;
    CG(doc_comment_len) = 0;
}

// 作用和上面switch,foreach是一樣的,函數體內的語句並不屬於函數體外的label
zend_stack_push(&CG(labels_stack), (void *) &CG(labels), sizeof(HashTable*));
CG(labels) = 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。舉例來說,

<?php
label1:
function foo($arg1)
{
    print($arg1);
    goto label2;
    
label2:
    exit;
}

$bar = 'hello php';
foo($bar);

解釋器在進入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數組中獲取變量,會更加高效。

void fetch_simple_variable_ex(znode *result, znode *varname, int bp, zend_uchar op TSRMLS_DC) /* {{{ */
{
    zend_op opline;
    ...

    if (varname->op_type == IS_CONST) {
        if (Z_TYPE(varname->u.constant) != IS_STRING) {
            convert_to_string(&varname->u.constant);
        }
        if (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len TSRMLS_CC) &&
            !(varname->u.constant.value.str.len == (sizeof("this")-1) && !memcmp(varname->u.constant.value.str.val, "this", sizeof("this"))) &&
            (CG(active_op_array)->last == 0 || CG(active_op_array)->opcodes[CG(active_op_array)->last-1].opcode != ZEND_BEGIN_SILENCE)) {
            
            // 節點的類型為IS_CV,表明是compiled variables
            result->op_type = IS_CV;
            
            // 用u.var來記錄compiled variables在CG(active_op_array)->vars中的索引
            result->u.var = lookup_cv(CG(active_op_array), varname->u.constant.value.str.val, varname->u.constant.value.str.len);
            result->u.EA.type = 0;
            varname->u.constant.value.str.val = CG(active_op_array)->vars[result->u.var].name;
            return;
        }
    }
    ...
}

這裡不做詳細的分析了。當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 *opline;
zend_arg_info *cur_arg_info;

// class_type主要用於限制函數參數的類型
if (class_type->op_type == IS_CONST && Z_TYPE(class_type->u.constant) == IS_STRING && Z_STRLEN(class_type->u.constant) == 0) {
    /* Usage of namespace as class name not in namespace */
    zval_dtor(&class_type->u.constant);
    zend_error(E_COMPILE_ERROR, "Cannot use 'namespace' as a class name");
    return;
}

// 對靜態方法來說,參數不能為this
if (var->op_type == IS_CV && var->u.var == CG(active_op_array)->this_var && (CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == 0) {
    zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");
} else if (var->op_type == IS_VAR && CG(active_op_array)->scope && ((CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == 0) && (Z_TYPE(varname->u.constant) == IS_STRING) && (Z_STRLEN(varname->u.constant) == sizeof("this")-1) && (memcmp(Z_STRVAL(varname->u.constant), "this", sizeof("this")) == 0)) {
    zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");
}

// CG(active_op_array)此時已經是函數體的op_array了,這裡拿一條指令
opline = get_next_op(CG(active_op_array) TSRMLS_CC);
CG(active_op_array)->num_args++;
opline->opcode = op;
opline->result = *var;

// op1節點表明是第幾個參數
opline->op1 = *offset;

// op2節點可能為初始值,也可能為UNUSED
if (op == ZEND_RECV_INIT) {
    opline->op2 = *initialization;
} else {
    CG(active_op_array)->required_num_args = CG(active_op_array)->num_args;
    SET_UNUSED(opline->op2);
}

上面這段代碼,首先通過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。

繼續來看:

// 這裡采用erealloc進行分配,因為期望最終會形成一個參數信息的數組
CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info, sizeof(zend_arg_info)*(CG(active_op_array)->num_args));

// 設置當前的zend_arg_info
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];
cur_arg_info->name = estrndup(varname->u.constant.value.str.val, varname->u.constant.value.str.len);
cur_arg_info->name_len = varname->u.constant.value.str.len;
cur_arg_info->array_type_hint = 0;
cur_arg_info->allow_null = 1;
cur_arg_info->pass_by_reference = pass_by_reference;
cur_arg_info->class_name = NULL;
cur_arg_info->class_name_len = 0;

// 如果需要對參數做類型限定
if (class_type->op_type != IS_UNUSED) {
    cur_arg_info->allow_null = 0;
    
    // 限定為類
    if (class_type->u.constant.type == IS_STRING) {
        if (ZEND_FETCH_CLASS_DEFAULT == zend_get_class_fetch_type(Z_STRVAL(class_type->u.constant), Z_STRLEN(class_type->u.constant))) {
            zend_resolve_class_name(class_type, &opline->extended_value, 1 TSRMLS_CC);
        }
        cur_arg_info->class_name = class_type->u.constant.value.str.val;
        cur_arg_info->class_name_len = class_type->u.constant.value.str.len;
        
        // 如果限定為類,則參數的默認值只能為NULL
        if (op == ZEND_RECV_INIT) {
            if (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), "NULL"))) {
                cur_arg_info->allow_null = 1;
            } else {
                zend_error(E_COMPILE_ERROR, "Default value for parameters with a class type hint can only be NULL");
            }
        }
    }
    // 限定為數組
    else {
        // 將array_type_hint設置為1
        cur_arg_info->array_type_hint = 1;
        cur_arg_info->class_name = NULL;
        cur_arg_info->class_name_len = 0;
        
        // 如果限定為數組,則參數的默認值只能為數組或NULL
        if (op == ZEND_RECV_INIT) {
            if (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), "NULL"))) {
                cur_arg_info->allow_null = 1;
            } else if (Z_TYPE(initialization->u.constant) != IS_ARRAY && Z_TYPE(initialization->u.constant) != IS_CONSTANT_ARRAY) {
                zend_error(E_COMPILE_ERROR, "Default value for parameters with array type hint can only be an array or NULL");
            }
        }
    }
}
opline->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函數會修正類名。舉例來說:

<?php
namespace A;
class B { }
function foo(B $arg1, $arg2 = 100)
{
    print($arg1);
}

我們期望參數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); }
;

函數體內部的語句,表示為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。

char lcname[16];
int name_len;

zend_do_extended_info(TSRMLS_C);

// 返回NULL
zend_do_return(NULL, 0 TSRMLS_CC);

// 通過op指令設置對應的handler函數
pass_two(CG(active_op_array) TSRMLS_CC);

// 釋放當前函數的CG(labels),並從CG(labels_stack)中還原之前的CG(labels)
zend_release_labels(TSRMLS_C);

if (CG(active_class_entry)) {
    // 檢查魔術方法的參數是否合法
    zend_check_magic_method_implementation(CG(active_class_entry), (zend_function*)CG(active_op_array), E_COMPILE_ERROR TSRMLS_CC);
} else {
    /* we don't care if the function name is longer, in fact lowercasing only 
     * the beginning of the name speeds up the check process */
    name_len = strlen(CG(active_op_array)->function_name);
    zend_str_tolower_copy(lcname, CG(active_op_array)->function_name, MIN(name_len, sizeof(lcname)-1));
    lcname[sizeof(lcname)-1] = '\0'; /* zend_str_tolower_copy won't necessarily set the zero byte */
    
    // 檢查__autoload函數的參數是否合法
    if (name_len == sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1 && !memcmp(lcname, ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME)) && CG(active_op_array)->num_args != 1) {
        zend_error(E_COMPILE_ERROR, "%s() must take exactly 1 argument", ZEND_AUTOLOAD_FUNC_NAME);
    }        
}

CG(active_op_array)->line_end = zend_get_compiled_lineno(TSRMLS_C);

// 很關鍵!將CG(active_op_array)還原成函數外層的op_array
CG(active_op_array) = function_token->u.op_array;

/* Pop the switch and foreach seperators */
zend_stack_del_top(&CG(switch_cond_stack));
zend_stack_del_top(&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指令。舉例來說:

function foo()
{ return true; }

編譯出來的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)宏來完成的。

#define ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)

zend_vm_set_opcode_handler的實現很簡單:

void zend_init_opcodes_handlers(void)
{
    // 超大的數組,裡面存放了所有的handler
    static const opcode_handler_t labels[] = {
        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 = (opcode_handler_t*)labels;
}

static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
        static const int zend_vm_decode[] = {
            _UNUSED_CODE, /* 0              */
            _CONST_CODE,  /* 1 = IS_CONST   */
            _TMP_CODE,    /* 2 = IS_TMP_VAR */
            _UNUSED_CODE, /* 3              */
            _VAR_CODE,    /* 4 = IS_VAR     */
            _UNUSED_CODE, /* 5              */
            _UNUSED_CODE, /* 6              */
            _UNUSED_CODE, /* 7              */
            _UNUSED_CODE, /* 8 = IS_UNUSED  */
            _UNUSED_CODE, /* 9              */
            _UNUSED_CODE, /* 10             */
            _UNUSED_CODE, /* 11             */
            _UNUSED_CODE, /* 12             */
            _UNUSED_CODE, /* 13             */
            _UNUSED_CODE, /* 14             */
            _UNUSED_CODE, /* 15             */
            _CV_CODE      /* 16 = IS_CV     */
        };
        
        // 去handler數組裡找到對應的處理函數
        return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];
}

ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
    // 給zend op設置對應的handler函數
    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}

所有的opcode都定義在zend_vm_opcodes.h裡,從php5.3-php5.6,大概從150增長到170個opcode。上面可以看到通過opcode查找handler的准確算法:

zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + 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:

// 拿到的是最近一條zend op,對於函數來說,就是ZEND_DECLARE_FUNCTION
zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1];
HashTable *table;

while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) {
    opline--;
}

switch (opline->opcode) {
    case ZEND_DECLARE_FUNCTION:
        // 真正綁定函數
        if (do_bind_function(opline, CG(function_table), 1) == FAILURE) {
            return;
        }
        table = CG(function_table);
        break;
    case ZEND_DECLARE_CLASS:
        ...
    case ZEND_DECLARE_INHERITED_CLASS:
        ...
}

// op1中保存的是函數的key,這裡其從將CG(function_table)中刪除
zend_hash_del(table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
zval_dtor(&opline->op1.u.constant);
zval_dtor(&opline->op2.u.constant);

// opline置為NOP
MAKE_NOP(opline);

這個函數實現也很簡單,主要就是調用了do_bind_function。

ZEND_API int do_bind_function(zend_op *opline, HashTable *function_table, zend_bool compile_time) /* {{{ */
{
    zend_function *function;

    // 找出函數
    zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (void *) &function);
    
    // 以函數名稱作為key,重新加入function_table
    if (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, function, sizeof(zend_function), NULL)==FAILURE) {
        int error_level = compile_time ? E_COMPILE_ERROR : E_ERROR;
        zend_function *old_function;

        // 加入失敗,可能發生重復定義了
        if (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+1, (void *) &old_function)==SUCCESS
            && old_function->type == ZEND_USER_FUNCTION
            && old_function->op_array.last > 0) {
            zend_error(error_level, "Cannot redeclare %s() (previously declared in %s:%d)",
                        function->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[0].lineno);
        } else {
            zend_error(error_level, "Cannot redeclare %s()", function->common.function_name);
        }
        return FAILURE;
    } else {
        (*function->op_array.refcount)++;
        function->op_array.static_variables = NULL; /* NULL out the unbound function */
        return SUCCESS;
    }
}

在進入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。一些關鍵的步驟如下圖:

 

至於函數的調用,又是另外一個話題了。

 

 

  1. 上一頁:
  2. 下一頁: