【PHP內核】語法:不同類型之間數值運算的實現

我們都知道php屬於弱類型的語言,不同類型之間可以直接進行運算,比如加減乘除,但是php是構建在c語言之上的,它是如何實現這種複合類型運算的呢?很顯然,內核幫我們作了類型轉化,下面我們就從一個簡單的例子具體看下zend引擎中都幹了哪些事。(文中涉及的代碼均來自php-7.0.4)

//a.php
<?php
$str = "6";
$a = $str + 5;

echo $a;
$ php a.php
$ 11

opcode及zend_execute_data

cli下執行一個php腳本的主要的流程是:main() -> do_cli() -> php_execute_script() -> zend_execute_scripts() -> (解析、編譯)compile_file() ->(執行) zend_execute()

我們直接跳過前面詞法、語法分析、AST的生成過程,直接從zend_execute開始看。

//zend_vm_execute.h #441
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)

這個方法比較簡單,只有兩個參數:編譯階段生成的opcode array、返回值指針,這個方法是vm執行的入口,所有的php腳本最終都是在這裏開始執行的。opcode是zend引擎的執行指令,比如加減、賦值、調用函數等等,所有的opcode定義在zend_vm_opcodes.h中。下面是opcode指令具體的結構:

//zend_compile.h #155
struct _zend_op {
    const void *handler; //該指令的處理函數
    znode_op op1; //操作數1,與op2用於存儲具體操作的左右值,如賦值操作,在編譯時將等號左右的兩個值分別存於op1、op2
    znode_op op2; //操作數2,可以不用
    znode_op result; //返回值
    uint32_t extended_value;
    uint32_t lineno;
    zend_uchar opcode; //opcode編碼
    zend_uchar op1_type; //操作數的類型:IS_CONST、IS_TMP_VAR、IS_VAR、IS_UNUSED、IS_CV
    zend_uchar op2_type;
    zend_uchar result_type;
};
//#73
typedef union _znode_op {
    uint32_t      constant;
    uint32_t      var; //存儲值在CV數組中的位置,所以是一個int,臨時變量存在一個CV數組中
    uint32_t      num;
    uint32_t      opline_num; /*  Needs to be signed */
#if ZEND_USE_ABS_JMP_ADDR
    zend_op       *jmp_addr;
#else
    uint32_t      jmp_offset;
#endif
#if ZEND_USE_ABS_CONST_ADDR
    zval          *zv;
#endif
} znode_op;

php腳本編譯的過程就是從AST生成一個個zend_op的結構,然後將opcodes數組傳給zend_execute()執行。
執行的過程中有一個非常核心的結構:zend_execute_data,這個結構定義也定義在zend_compile.h中:

#430
struct _zend_execute_data {
    const zend_op       *opline;           /* executed opline                */
    zend_execute_data   *call;             /* current call                   */
    zval                *return_value;
    zend_function       *func;             /* executed funcrion              */
    zval                 This;             /* this + call_info + num_args    */
    zend_class_entry    *called_scope;
    zend_execute_data   *prev_execute_data;
    zend_array          *symbol_table;
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;   /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
    zval                *literals;         /* cache op_array->literals       */
#endif
};

zend_execute_data這個結構可以簡單的認爲是一個運行棧,它記錄着執行過程中的opcode、符號表等等,最終執行的過程就是從zend_execute_data->opline開始,然後zend_execute_data->opline++執行下一條指令。

函數調用會新開闢一個zend_execute_data,接着初始化,返回ZEND_VM_ENTER進入新的execute,然後開始從新的zend_execute_data->opline開始執行函數內部的opcode,執行完再將之前的zend_execute_data指針還原,接着執行下面的操作。關於函數、類的執行機制這裏不多說,後續會有專門的介紹。

下面回到文章一開始提到的那個例子。

弱類型運算

用gdb調試zend_execute(),根據傳入的op_array得到所有的opcode:

opcodes

handler是根據opcode、op1_type、op2_type確定的,換句話說,每一個opcode都可以根據不同的操作數類型定義不同的handler,所以一個opcode最多有5x5=25個handler,在定義的時候也需要定義25個,當然定義爲null,具體的對應方法見:

//zend_vm_execute.h #49741
ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
    op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}
//#49717
static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const zend_op* op)
{
    .....
    return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]];
}

opcode handler也全部定
義在zend_vm_execute.h,從php的代碼可以看出各語句對應的opcode:

<?php
$str = "6";     => ZEND_ASSIGN
$a = $str + 5;  => ZEND_ADD & ZEND_ASSIGN

echo $a;        => ZEND_ECHO

string + int的加法運算opcode就是ZEND_ADD,對應的handler是ZEND_ADD_SPEC_CV_CONST_HANDLER

//zend_vm_execute.h #29773
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *op1, *op2, *result;

    op1 = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);
    op2 = EX_CONSTANT(opline->op2);
    //這裏是針對long、double類型的直接處理(數值類型之間轉化比較簡單,本文不對這種情況討論)
    if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
        ...
    } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
        ...
    }

    SAVE_OPLINE();
    if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) {
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) {
        op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
    }
    //非數值類型將走到這裏處理
    add_function(EX_VAR(opline->result.var), op1, op2);

    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

示例中是一個string + int的操作,有非數值類型,所以會由add_function()處理:

//zend_operators.c #865
ZEND_API int ZEND_FASTCALL add_function(zval *result, zval *op1, zval *op2)
{
    while (1) {
        switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {
            ...
            defualt:
                ...
                ZEND_TRY_BINARY_OBJECT_OPERATION(ZEND_ADD, add_function);
                zendi_convert_scalar_to_number(op1, op1_copy, result);
                zendi_convert_scalar_to_number(op2, op2_copy, result);
        }
   }
}

看到了吧zendi_convert_scalar_to_number(),這就是內核幫我們轉化類型的地方,到這裏我們應該就能明白php不同類型之間運算的實現方式了吧,再具體追下zendi_convert_scalar_to_number,這其實是個宏:

//zend_operators.c #190
#define zendi_convert_scalar_to_number(op, holder, result)          \
    if (op==result) {                                               \
        if (Z_TYPE_P(op) != IS_LONG) {                              \
            convert_scalar_to_number(op);                   \
        }                                                           \
    } else {                                                        \
        switch (Z_TYPE_P(op)) {                                     \
            case IS_STRING:                                         \
                {                                                   \
                    if ((Z_TYPE_INFO(holder)=is_numeric_string(Z_STRVAL_P(op), Z_STRLEN_P(op), &Z_LVAL(holder), &Z_DVAL(holder), 1)) == 0) {    \
                        ZVAL_LONG(&(holder), 0);                            \
                    }                                                       \
                    (op) = &(holder);                                       \
                    break;                                                  \
                }                                                           \
            case IS_NULL:  
            ...

op2是IS_LONG,不需要處理,這裏只有op1從string -> long,這個宏傳了三個參數:op1,op1_copy,result,轉化爲long的值放到了op1_copy中,然後替換爲op1,這時候add_function()下一次循環就到“case TYPE_PAIR(IS_LONG, IS_LONG): ”處理了,從這裏我們看出內核是對變量轉化後的新值進行的運算,對原變量並沒有作處理

具體的類型轉化可以看is_numeric_string()方法,這裏是根據字符(+、-、.)確定是轉爲long還是double的,具體過程有興趣的可以仔細看下算法:

//zend_operators.h #138
static zend_always_inline zend_uchar is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info)
{
    if (*str > '9') {
        return 0;
    }
    return _is_numeric_string_ex(str, length, lval, dval, allow_errors, oflow_info);
}

static zend_always_inline zend_uchar is_numeric_string(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors) {
    return is_numeric_string_ex(str, length, lval, dval, allow_errors, NULL);
}

//zend_operators.c #2753
ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info)
{
    ...
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章