PHP黑魔法深度剖析(一)——PHP的弱类型比较
2019-06-19 10:32:00 Author: xz.aliyun.com(查看原文) 阅读量:145 收藏

最近一直在学习PHP源码,在对PHP各个变量的实现有了一个大概的了解之后,尝试着对PHP的一些特性进行分析。在PHP源码分析方面,我算是一个初学者,如果有错误,欢迎师傅们批评指正。

前言

PHP中有很多黑魔法,最初入门CTF的时候,就经常遇到考察PHP弱类型的题,比如

<?php 
error_reporting(0); 
include_once('flag.php'); 
highlight_file('index.php');  

$md51 = md5('QNKCDZO'); 
$a = $_GET['b']; 
$md52 = md5($a); 
if(isset($a)){ 
    if ($a != 'QNKCDZO' && $md51 == $md52) { 
        echo $flag; 
    } else { 
    echo "false!!!"; 
    }
}

解决方案就是寻找一个MD5值是0e开头的字符串,PHP在使用==进行比较的时候,会认为该字符串是科学计数法表示的数字,然后又因为QNKCDZO的MD5值是0e830400451993494058024219903391,两个字符串都被转换为数字0,从而使表达式$md51 == $md52成立,但是如果是===运算符,表达式就不会成立了。
对于变量之间的比较,手册中写的也挺详细的。

接下来根据PHP的源码来分析下,这两个运算符是如何实现的。

环境&工具

  • Mac Mojave 10.14
  • PHP 7.1 + vld扩展
  • VSCode debug
  • UnderStand

1. PHP的弱类型实现

我们都知道PHP中的变量本身是弱类型的,使用者在使用时不需要对变量类型进行声明,但PHP的底层是用C语言实现的,而C语言中的变量是强类型的,使用时需要对变量类型进行声明。接下来我们基于PHP7的源码,来简单分析下PHP中的变量实现。

PHP中,所有的变量都是由一个zval结构体来存储的。
路径:Zend/zend_types.h:121-143

struct _zval_struct {
    zend_value value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar type,         /* zval类型 */
                zend_uchar type_flags, /* 对应变量类型特有的标记 */
                zend_uchar const_flags, /* 常量类型的标记 */
                zend_uchar reserved)  /* call info for EX(This) */
        } v;
        uint32_t type_info; /* 与v是一个联合体,内存共享,修改该值等于修改结构体v的值。 */
    } u1;
    union {
        uint32_t next; /* 用来解决hash冲突 */
        uint32_t cache_slot; /* 运行时的缓存 */
        uint32_t lineno; /* zend_ast_zval存行号 */
        uint32_t num_args; /* EX(This)参数个数 */
        uint32_t fe_pos; /* foreach的位置 */
        uint32_t fe_iter_idx; /* foreach 游标的标记 */
        uint32_t access_flags; /* 类的常量访问标识 */
        // 常用的标识有 public、protected、 private
        uint32_t property_guard; /* 单一属性保护 */
        // 防止类中魔术方法的循环调用
    } u2;
};

变量真正的数据存储在value中,也就是结构体_zend_value

typedef union _zend_value {
    zend_long lval; // 整型
    double dval; // 浮点型
    zend_refcounted *counted; // 引用计数
    zend_string *str; // 字符串类型
    zend_array *arr; // 数组类型
    zend_object *obj; // 对象类型
    zend_resource *res; // 资源类型
    zend_reference *ref; // 引用类型
    zend_ast_ref *ast; // 抽象语法树
    zval *zv; // zval类型
    void *ptr; // 指针类型
    zend_class_entry *ce; // class类型
    zend_function *func; // function类型
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

而变量的类型通过联合体v中的type来表示。
路径Zend/zend_types.h:303

/* 常规数据类型 */
#define IS_UNDEF                    0 // 标记未使用类型
#define IS_NULL                     1 // NULL
#define IS_FALSE                    2 // 布尔false
#define IS_TRUE                     3 // 布尔true
#define IS_LONG                     4 // 长整型
#define IS_DOUBLE                   5 // 浮点型
#define IS_STRING                   6 // 字符串
#define IS_ARRAY                    7 // 数组
#define IS_OBJECT                   8 // 对象
#define IS_RESOURCE                 9 // 资源类型
#define IS_REFERENCE                10 // 参考类型
/* constant expressions */
#define IS_CONSTANT                 11 // 常量类型
#define IS_CONSTANT_AST             12 // 常量类型的AST数
/* 伪类型 */
#define _IS_BOOL                    13
#define IS_CALLABLE                 14
#define IS_ITERABLE                 19
#define IS_VOID                     18
/* 内部类型 */
#define IS_INDIRECT                 15 // 间接类型
#define IS_PTR                      17 // 指针类型
#define _IS_ERROR                   20 // 错误类型

在真正取值的时候,Zend虚拟机会根据获取的type类型来获取对应的值。
比如我们执行代码$a = 1;,在PHP内部,$azval结构体来表示,它的u1.v.type==IS_LONG,这表示它是一个长整型,它的value.lval==1,这表示它的值为1
如果代码是$b = '123';,那么它的u1.v.type==IS_STRING,这表示它是一个字符串,它的value == zend_string *str,真正的字符串123存储在PHP中的zend_string结构体中。
总之,在PHP中,无论是什么类型的变量,都是在zval结构体中存储的,Zend虚拟机面对的,始终是zval结构体。
基于这种结构,PHP中的变量成功实现了弱类型。

接下来我们看一下PHP弱类型比较的实现过程。

2. '==' && '===' 的源码实现

2.1 前置知识

首先我们先了解一下PHP的执行过程。

  1. 进行词法分析,将PHP代码转换为有意义的标识Token,使用词法分析器Re2c实现,将Zend/zend_language_scanner.l文件编译为Zend/zend_language_scanner.c
  2. 进行语法分析,将Token和符合文法规则的代码生成抽象语法树。语法分析器基于Bison实现,将Zend/zend_language_parser.y文件编译为Zend/zend_language_parser.c
  3. 生成的抽象语法树生成对应的opcode,然后被虚拟机执行。opcode对应着相应的处理函数,当虚拟机调用opcode时,会找到opcode对应的处理函数,执行真正的处理过程。

2.2 分析过程

测试代码

<?php
$a = "123";
var_dump($a == 123);
var_dump($a === 123);

我们借助vld扩展来看一下代码执行的opcode

可以看到,我们拿到了两个比较符对应的opcode,很容易理解。

'==' : IS_EQUAL // 相等
'===': IS_IDENTICAL // 完全相等

然后我们根据拿到的这两个opcode,查找词法分析的源码来验证下。
路径:Zend/zend_language_scanner.l:1468

<ST_IN_SCRIPTING>"===" {
    RETURN_TOKEN(T_IS_IDENTICAL);
}

路径:Zend/zend_language_scanner.l:1476

<ST_IN_SCRIPTING>"==" {
    RETURN_TOKEN(T_IS_EQUAL);
}

我们可以知道,在词法分析时,标识TokenT_IS_EQUALT_IS_IDENTICAL
接下来语法分析的源码Zend/zend_language_parser.y中查找。
路径:Zend/zend_language_parser.y:931

|   expr T_IS_IDENTICAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_IDENTICAL, $1, $3); }

路径:Zend/zend_language_parser.y:935

|   expr T_IS_EQUAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_EQUAL, $1, $3); }

可以知道,在语法分析中,调用生成opcode的函数为zend_ast_create_binary_op,生成的opcode分别是ZEND_IS_EQUALZEND_IS_IDENTICAL

接下来就是去寻找opcode对应的处理函数了。
路径:Zend/zend_vm_execute.h

根据Token可以搜索到很多函数的声明,根据函数名以及我们上面的vld扩展的输出,我们可以猜测,命名规则为
ZEND_IS_EQUAL_SPEC_开头,接下来是OP1OP2,然后以HANDLE结尾。


ZEND_IS_IDENTICAL对应函数的的声明也类似。

2.2.1 '==' 源码实现分析

根据vld扩展的输出,我们找到对应的函数ZEND_IS_EQUAL_SPEC_CV_CONST_HANDLER

路径:Zend/zend_vm_execute.h:36530

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_IS_EQUAL_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); // 获取OP1
    op2 = EX_CONSTANT(opline->op2); // 获取OP2
    do {
        int result;

        if (EXPECTED(Z_TYPE_P(op1) == IS_LONG)) {
            if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) { // 如果OP1和OP2都是长整型,直接作比较并获得结果
                result = (Z_LVAL_P(op1) == Z_LVAL_P(op2));
            } else if (EXPECTED(Z_TYPE_P(op2) == IS_DOUBLE)) { // 如果OP1是长整型,OP2是浮点型,对OP1进行强制类型转换为浮点型,然后再作比较。
                result = ((double)Z_LVAL_P(op1) == Z_DVAL_P(op2));
            } else {
                break; // 跳出
            }
        } else if (EXPECTED(Z_TYPE_P(op1) == IS_DOUBLE)) { 
            if (EXPECTED(Z_TYPE_P(op2) == IS_DOUBLE)) { // 如果OP1和OP2都是浮点型,直接作比较并获得结果
                result = (Z_DVAL_P(op1) == Z_DVAL_P(op2));
            } else if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) { // 如果OP1是浮点型,OP2是长整型,对OP2进行强制类型转换为浮点型,然后再作比较
                result = (Z_DVAL_P(op1) == ((double)Z_LVAL_P(op2)));
            } else {
                break; // 跳出
            }
        } else if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
            if (EXPECTED(Z_TYPE_P(op2) == IS_STRING)) { // 如果OP1和OP2都是字符串
                if (Z_STR_P(op1) == Z_STR_P(op2)) { // 取出OP1和OP2的zval.value.str结构体,判断是否相等
                    result = 1;
                } else if (Z_STRVAL_P(op1)[0] > '9' || Z_STRVAL_P(op2)[0] > '9') { // 如果OP1或者OP2的字符串开头不是数字
                    if (Z_STRLEN_P(op1) != Z_STRLEN_P(op2)) { // 两个字符串长度不相同
                        result = 0;
                    } else {
                        result = (memcmp(Z_STRVAL_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op1)) == 0);// 按字节来判断OP1和OP2的字符串结构体是否相等
                    }
                } else {
                    result = (zendi_smart_strcmp(Z_STR_P(op1), Z_STR_P(op2)) == 0); // 使用zendi_smart_strcmp来判断OP1和OP2的字符串是否相等
                }


            } else {
                break;
            }
        } else {
            break;
        }
        ZEND_VM_SMART_BRANCH(result, 0);
        ZVAL_BOOL(EX_VAR(opline->result.var), result);
        ZEND_VM_NEXT_OPCODE();
    } while (0);

    SAVE_OPLINE();
    if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_P(op1) == IS_UNDEF)) { // 异常判断
        op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
    }
    if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_P(op2) == IS_UNDEF)) { // 异常判断
        op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
    }
    result = EX_VAR(opline->result.var);
    compare_function(result, op1, op2); // 后面进行重点分析
    ZVAL_BOOL(result, Z_LVAL_P(result) == 0); // 将结果转换为布尔型


    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); // Zend虚拟机执行下一个opcode
}

可以看到,如果前面的条件都没能成立,就会进入compare_function函数。
首先我们查看一下调用关系,可以知道compare_functionPHP中变量对比的一个核心函数,

为了方便阅读,我把其中用到的宏放到了下面。

#define TYPE_PAIR(t1,t2) (((t1) << 4) | (t2))

#define Z_DVAL(zval)                (zval).value.dval
#define Z_DVAL_P(zval_p)            Z_DVAL(*(zval_p))

#define ZVAL_LONG(z, l) // 将zval z的类型设置为长整型,值设置为l

路径:Zend/zend_operators.c:1976

ZEND_API int ZEND_FASTCALL compare_function(zval *result, zval *op1, zval *op2)
{
    int ret;
    int converted = 0;
    zval op1_copy, op2_copy;
    zval *op_free, tmp_free;

    while (1) {
        switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) { // 获取OP1和OP2的type值,然后进行TYPE_PAIR运算
            case TYPE_PAIR(IS_LONG, IS_LONG): // 两者都是长整型
                ZVAL_LONG(result, Z_LVAL_P(op1)>Z_LVAL_P(op2)?1:(Z_LVAL_P(op1)<Z_LVAL_P(op2)?-1:0));
                return SUCCESS;

            case TYPE_PAIR(IS_DOUBLE, IS_LONG): // OP1为浮点型,OP2为长整型
                Z_DVAL_P(result) = Z_DVAL_P(op1) - (double)Z_LVAL_P(op2);
                ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_DVAL_P(result)));
                return SUCCESS;

            case TYPE_PAIR(IS_LONG, IS_DOUBLE): // OP1为长整型,OP2位浮点型
                Z_DVAL_P(result) = (double)Z_LVAL_P(op1) - Z_DVAL_P(op2);
                ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_DVAL_P(result)));
                return SUCCESS;

            case TYPE_PAIR(IS_DOUBLE, IS_DOUBLE): // OP1和OP2都为浮点型
                if (Z_DVAL_P(op1) == Z_DVAL_P(op2)) { // 直接获取浮点数来做对比
                    ZVAL_LONG(result, 0);
                } else {
                    Z_DVAL_P(result) = Z_DVAL_P(op1) - Z_DVAL_P(op2);
                    ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_DVAL_P(result)));
                }
                return SUCCESS;

            case TYPE_PAIR(IS_ARRAY, IS_ARRAY): // OP1和OP2都为数组
                ZVAL_LONG(result, zend_compare_arrays(op1, op2));
                return SUCCESS;

            case TYPE_PAIR(IS_NULL, IS_NULL): // OP1和OP2都为NULL
            case TYPE_PAIR(IS_NULL, IS_FALSE): // OP1为NULL,OP2为布尔型false
            case TYPE_PAIR(IS_FALSE, IS_NULL): // OP1为布尔型false,OP2为NULL
            case TYPE_PAIR(IS_FALSE, IS_FALSE): // OP1和OP2都为布尔型false
            case TYPE_PAIR(IS_TRUE, IS_TRUE): // OP1和OP2都为布尔型true
                ZVAL_LONG(result, 0);
                return SUCCESS;

            case TYPE_PAIR(IS_NULL, IS_TRUE): // OP1为NULL,OP2为布尔型true
                ZVAL_LONG(result, -1);
                return SUCCESS;

            case TYPE_PAIR(IS_TRUE, IS_NULL): // OP1为布尔型true,OP2为NULL
                ZVAL_LONG(result, 1);
                return SUCCESS;

            case TYPE_PAIR(IS_STRING, IS_STRING): // OP1和OP2都为字符串
                if (Z_STR_P(op1) == Z_STR_P(op2)) {
                    ZVAL_LONG(result, 0);
                    return SUCCESS;
                }
                ZVAL_LONG(result, zendi_smart_strcmp(Z_STR_P(op1), Z_STR_P(op2)));
                return SUCCESS;

            case TYPE_PAIR(IS_NULL, IS_STRING): // OP1是NULL,OP2是字符串
                ZVAL_LONG(result, Z_STRLEN_P(op2) == 0 ? 0 : -1);
                return SUCCESS;

            case TYPE_PAIR(IS_STRING, IS_NULL): // OP1是字符串,OP2是NULL
                ZVAL_LONG(result, Z_STRLEN_P(op1) == 0 ? 0 : 1);
                return SUCCESS;

            case TYPE_PAIR(IS_OBJECT, IS_NULL): // OP1是对象,OP2是NULL
                ZVAL_LONG(result, 1);
                return SUCCESS;

            case TYPE_PAIR(IS_NULL, IS_OBJECT): // OP1是NULL,OP2是对象
                ZVAL_LONG(result, -1);
                return SUCCESS;

            default:
                ......

在最后的default部分,我们会用到PHP对象存储的相关知识,先来看下了解下PHP中对象的存储结构。
路径:Zend/zend_types.h:276

struct _zend_object {
    zend_refcounted_h gc; // GC头部
    uint32_t          handle; // 结构体在全局变量中的索引
    zend_class_entry *ce; // 所属的类结构体指针
    const zend_object_handlers *handlers; // 指向对对象进行操作的多个指针函数
    HashTable        *properties; // 存储对象的动态属性值
    zval              properties_table[1]; // 柔性数组,存储对象的普通属性值
};

以下是对对象进行操作的函数结构体定义,根据命名就能明白各个函数的功能是什么。
路径:Zend/zend_object_handlers.h:124

struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
    zend_object_dtor_obj_t                  dtor_obj;
    zend_object_clone_obj_t                 clone_obj;
    /* individual object functions */
    zend_object_read_property_t             read_property;
    zend_object_write_property_t            write_property;
    zend_object_read_dimension_t            read_dimension;
    zend_object_write_dimension_t           write_dimension;
    zend_object_get_property_ptr_ptr_t      get_property_ptr_ptr;
    zend_object_get_t                       get;
    zend_object_set_t                       set;
    zend_object_has_property_t              has_property;
    zend_object_unset_property_t            unset_property;
    zend_object_has_dimension_t             has_dimension;
    zend_object_unset_dimension_t           unset_dimension;
    zend_object_get_properties_t            get_properties;
    zend_object_get_method_t                get_method;
    zend_object_call_method_t               call_method;
    zend_object_get_constructor_t           get_constructor;
    zend_object_get_class_name_t            get_class_name;
    zend_object_compare_t                   compare_objects;
    zend_object_cast_t                      cast_object;
    zend_object_count_elements_t            count_elements;
    zend_object_get_debug_info_t            get_debug_info;
    zend_object_get_closure_t               get_closure;
    zend_object_get_gc_t                    get_gc;
    zend_object_do_operation_t              do_operation;
    zend_object_compare_zvals_t             compare;
};

大致了解了下对象的存储结构,我们接着往下看。

default:
                if (Z_ISREF_P(op1)) { // 如果OP1是引用类型
                    op1 = Z_REFVAL_P(op1); // 获取OP1真正指向的zval
                    continue;
                } else if (Z_ISREF_P(op2)) { // 如果OP1是引用类型
                    op2 = Z_REFVAL_P(op2); // 获取OP1真正指向的zval
                    continue;
                }

                if (Z_TYPE_P(op1) == IS_OBJECT && Z_OBJ_HANDLER_P(op1, compare)) { // 如果OP1是对象,并且OP1的handlers.compare函数存在
                    ret = Z_OBJ_HANDLER_P(op1, compare)(result, op1, op2); // 使用OP1的handlers.compare函数进行对比操作
                    if (UNEXPECTED(Z_TYPE_P(result) != IS_LONG)) {
                        convert_compare_result_to_long(result);
                    }
                    return ret;
                } else if (Z_TYPE_P(op2) == IS_OBJECT && Z_OBJ_HANDLER_P(op2, compare)) { // 如果OP2是对象,并且OP2的handlers.compare函数存在
                    ret = Z_OBJ_HANDLER_P(op2, compare)(result, op1, op2); // 使用OP2的handlers.compare函数进行对比操作
                    if (UNEXPECTED(Z_TYPE_P(result) != IS_LONG)) {
                        convert_compare_result_to_long(result);
                    }
                    return ret;
                }

                if (Z_TYPE_P(op1) == IS_OBJECT && Z_TYPE_P(op2) == IS_OBJECT) { // 如果OP1和OP2都是对象
                    if (Z_OBJ_P(op1) == Z_OBJ_P(op2)) {
                        // 如果对象的handle相同,表示OP1和OP2是同一个对象
                        ZVAL_LONG(result, 0);
                        return SUCCESS;
                    }
                    if (Z_OBJ_HANDLER_P(op1, compare_objects) == Z_OBJ_HANDLER_P(op2, compare_objects)) { // 如果OP1.handlers.compare_objects函数与OP2的相同,则调用该函数进行对比
                        ZVAL_LONG(result, Z_OBJ_HANDLER_P(op1, compare_objects)(op1, op2));
                        return SUCCESS;
                    }
                }
                if (Z_TYPE_P(op1) == IS_OBJECT) { // 如果OP1是个对象
                    if (Z_OBJ_HT_P(op1)->get) { // OP1.handlers.get函数存在
                        zval rv;
                        op_free = Z_OBJ_HT_P(op1)->get(op1, &rv); // 获取OP1的值
                        ret = compare_function(result, op_free, op2); // 递归调用compare_function
                        zend_free_obj_get_result(op_free);
                        return ret;
                    } else if (Z_TYPE_P(op2) != IS_OBJECT && Z_OBJ_HT_P(op1)->cast_object) { // 如果OP2不是对象,并且OP1.handlers.cast_object函数(用来将对象转换为其他类型)存在
                        ZVAL_UNDEF(&tmp_free);
                        if (Z_OBJ_HT_P(op1)->cast_object(op1, &tmp_free, ((Z_TYPE_P(op2) == IS_FALSE || Z_TYPE_P(op2) == IS_TRUE) ? _IS_BOOL : Z_TYPE_P(op2))) == FAILURE) { // 如果OP2是布尔型,则将OP1转换为布尔型,否则转换失败
                            ZVAL_LONG(result, 1); // OP1 > OP2
                            zend_free_obj_get_result(&tmp_free);
                            return SUCCESS;
                        }
                        ret = compare_function(result, &tmp_free, op2);
                        zend_free_obj_get_result(&tmp_free);
                        return ret;
                    }
                }
                if (Z_TYPE_P(op2) == IS_OBJECT) { // 如果OP2是个对象
                    if (Z_OBJ_HT_P(op2)->get) { // OP2.handlers.get函数存在
                        zval rv;
                        op_free = Z_OBJ_HT_P(op2)->get(op2, &rv); // 获取OP2的值
                        ret = compare_function(result, op1, op_free); // 递归调用compare_function
                        zend_free_obj_get_result(op_free);
                        return ret;
                    } else if (Z_TYPE_P(op1) != IS_OBJECT && Z_OBJ_HT_P(op2)->cast_object) { // 如果OP1不是对象,并且OP2.handlers.cast_object函数(用来将对象转换为其他类型)存在
                        ZVAL_UNDEF(&tmp_free);
                        if (Z_OBJ_HT_P(op2)->cast_object(op2, &tmp_free, ((Z_TYPE_P(op1) == IS_FALSE || Z_TYPE_P(op1) == IS_TRUE) ? _IS_BOOL : Z_TYPE_P(op1))) == FAILURE) { // 如果OP1是布尔型,则将OP2转换为布尔型,否则转换失败
                            ZVAL_LONG(result, -1); // OP1 < OP2
                            zend_free_obj_get_result(&tmp_free);
                            return SUCCESS;
                        }
                        ret = compare_function(result, op1, &tmp_free);
                        zend_free_obj_get_result(&tmp_free);
                        return ret;
                    } else if (Z_TYPE_P(op1) == IS_OBJECT) {
                        ZVAL_LONG(result, 1);
                        return SUCCESS;
                    }
                }
                if (!converted) { // converted为0
                    if (Z_TYPE_P(op1) == IS_NULL || Z_TYPE_P(op1) == IS_FALSE) { // 如果OP1是NULL或者布尔型false
                        ZVAL_LONG(result, zval_is_true(op2) ? -1 : 0); // 如果OP2转换为布尔型是ture,则OP1 < OP2,否则,OP1 == OP2
                        return SUCCESS;
                    } else if (Z_TYPE_P(op2) == IS_NULL || Z_TYPE_P(op2) == IS_FALSE) { // 如果OP2是NULL或者布尔型false
                        ZVAL_LONG(result, zval_is_true(op1) ? 1 : 0); // 如果OP1转换为布尔型是ture,则OP1 > OP2,否则,OP1 == OP2
                        return SUCCESS;
                    } else if (Z_TYPE_P(op1) == IS_TRUE) { // 如果OP1是布尔型true
                        ZVAL_LONG(result, zval_is_true(op2) ? 0 : 1); // 如果OP2转换为布尔型是true,则OP1 == OP2,否则 OP1 > OP2
                        return SUCCESS;
                    } else if (Z_TYPE_P(op2) == IS_TRUE) { // 如果OP2是布尔型true
                        ZVAL_LONG(result, zval_is_true(op1) ? 0 : -1); // 如果OP1转换为布尔型是true,则OP1 == OP2,否则 OP1 < OP2
                        return SUCCESS;
                    } else {
                        zendi_convert_scalar_to_number(op1, op1_copy, result, 1); // 根据OP1的类型,转换为数字
                        zendi_convert_scalar_to_number(op2, op2_copy, result, 1); // 根据OP2的类型,转换为数字
                        converted = 1; // 标识已经经过了转换
                    }
                } else if (Z_TYPE_P(op1)==IS_ARRAY) { // 如果OP1的类型是数组
                    ZVAL_LONG(result, 1); // OP1 > OP2
                    return SUCCESS;
                } else if (Z_TYPE_P(op2)==IS_ARRAY) { // 如果OP2的类型是数组
                    ZVAL_LONG(result, -1); // OP1 < OP2
                    return SUCCESS;
                } else if (Z_TYPE_P(op1)==IS_OBJECT) { // 如果OP1的类型是对象
                    ZVAL_LONG(result, 1); // OP1 > OP2
                    return SUCCESS;
                } else if (Z_TYPE_P(op2)==IS_OBJECT) { // 如果OP2的类型是对象
                    ZVAL_LONG(result, -1); // OP1 < OP2
                    return SUCCESS;
                } else {
                    ZVAL_LONG(result, 0); // OP1 == OP2
                    return FAILURE;
                }
        }
    }
}

总体来看,在进行==运算的时候,虽然我们在写的时候只有短短的一句话,但是在PHP内核实现的时候,却是考虑到了各种可能的情况,还进行了类型转换,从而实现了一个松散的判断相等的运算符。
对于类型转换,重点就是宏zendi_convert_scalar_to_number,跟下去意义不是很大,有需要的可以查询官方手册

整个==运算符的功能实现大概就这么多,接下来我们来看一下===运算符的实现。

2.2.2 '===' 源码实现分析

根据我们前面的分析,寻找ZEND_IS_IDENTICAL_SPEC_CV_CONST_HANDLER函数。

路径:Zend/zend_vm_execute.h:36494

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_IS_IDENTICAL_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE

    zval *op1, *op2;
    int result;

    SAVE_OPLINE();
    op1 = _get_zval_ptr_cv_deref_BP_VAR_R(execute_data, opline->op1.var); // 获取OP1
    op2 = EX_CONSTANT(opline->op2); // 获取OP2
    result = fast_is_identical_function(op1, op2);


    ZEND_VM_SMART_BRANCH(result, 1);
    ZVAL_BOOL(EX_VAR(opline->result.var), result);
    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

很明显,函数在获取OP1OP2之后,进入了fast_is_identical_function函数,跟进一下。
路径:Zend/zend_operators.h:748

static zend_always_inline int fast_is_identical_function(zval *op1, zval *op2)
{
    if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) { // 如果OP1和OP2的类型不相同,返回0
        return 0;
    } else if (Z_TYPE_P(op1) <= IS_TRUE) { // 可以看前面定义的宏来判断,如果OP1的类型是IS_UNDEF、IS_NULL、IS_FALSE、IS_TRUE,则返回1
        return 1;
    }
    return zend_is_identical(op1, op2);
}

如果以上两个条件都不成立,进入zend_is_identical函数并返回它的返回值,继续跟进。

路径:Zend/zend_operators.c:2004

ZEND_API int ZEND_FASTCALL zend_is_identical(zval *op1, zval *op2) /* {{{ */
{
    if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) { // 如果OP1和OP2的类型不相同,返回0
        return 0;
    }
    switch (Z_TYPE_P(op1)) { // 获取OP1的类型
        case IS_NULL:
        case IS_FALSE:
        case IS_TRUE: // 如果是NULL和布尔型,则返回1
            return 1; 
        case IS_LONG: // 如果是长整型,直接获取值判断是否相等,并返回
            return (Z_LVAL_P(op1) == Z_LVAL_P(op2));
        case IS_RESOURCE: // 如果是资源类型,直接获取值判断是否相等,并返回
            return (Z_RES_P(op1) == Z_RES_P(op2));
        case IS_DOUBLE: // 如果是浮点型,直接获取值判断是否相等,并返回
            return (Z_DVAL_P(op1) == Z_DVAL_P(op2));
        case IS_STRING: // 如果是字符串,判断是否是同一个字符串,或者字符串值得长度相同,每一个字节都相同
            return (Z_STR_P(op1) == Z_STR_P(op2) ||
                (Z_STRLEN_P(op1) == Z_STRLEN_P(op2) &&
                 memcmp(Z_STRVAL_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op1)) == 0));
        case IS_ARRAY: // 如果是数组,判断是否为同一个数组,或者调用zend_hash_compare进行判断
            return (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2) ||
                zend_hash_compare(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2), (compare_func_t) hash_zval_identical_function, 1) == 0);
        case IS_OBJECT: // 如果是对象,判断对象的值和对象指向的handler是否相同
            return (Z_OBJ_P(op1) == Z_OBJ_P(op2) && Z_OBJ_HT_P(op1) == Z_OBJ_HT_P(op2));
        default:
            return 0; // 不是上述已知类型,则返回0
    }
}

经过以上分析我们可以知道,result1时,返回trueresult0时,返回false
===运算符在内部实现上要比==要简单的多,只有满足类型相同,对应的值也相同的变量才能满足条件,而且不会进行类型转换。
当然,在对变量值进行比较的过程中,不同的变量也会有不同的规则,比如数组。

在手册中,我们知道

具有较少成员的数组较小,如果运算数 1 中的键不存在于运算数 2 中则数组无法比较,否则挨个值比较

zend_is_identical中我们跟进zend_hash_compare,可以找到zend_hash_compare_impl
路径:Zend/zend_hash.c:2313

static zend_always_inline int zend_hash_compare_impl(HashTable *ht1, HashTable *ht2, compare_func_t compar, zend_bool ordered) {
    uint32_t idx1, idx2;

    if (ht1->nNumOfElements != ht2->nNumOfElements) { // 当长度不相同时,较长的数组大于较短的数组
        return ht1->nNumOfElements > ht2->nNumOfElements ? 1 : -1;
    }

    for (idx1 = 0, idx2 = 0; idx1 < ht1->nNumUsed; idx1++) { // 长度相同,遍历数组,挨个值进行比较。
        Bucket *p1 = ht1->arData + idx1, *p2;
        zval *pData1, *pData2;
        int result;

        if (Z_TYPE(p1->val) == IS_UNDEF) continue; // 如果类型未定义,直接继续
        if (ordered) {
            while (1) {
                ZEND_ASSERT(idx2 != ht2->nNumUsed);
                p2 = ht2->arData + idx2;
                if (Z_TYPE(p2->val) != IS_UNDEF) break;
                idx2++;
            }
            if (p1->key == NULL && p2->key == NULL) { /* 数字索引 */
                if (p1->h != p2->h) {
                    return p1->h > p2->h ? 1 : -1;
                }
            } else if (p1->key != NULL && p2->key != NULL) { /* 字符串索引 */
                if (ZSTR_LEN(p1->key) != ZSTR_LEN(p2->key)) {
                    return ZSTR_LEN(p1->key) > ZSTR_LEN(p2->key) ? 1 : -1;
                }

                result = memcmp(ZSTR_VAL(p1->key), ZSTR_VAL(p2->key), ZSTR_LEN(p1->key));// 获取两个key对应的值来进行对比
                if (result != 0) { // 当存在不相等的成员时,返回结果。
                    return result;
                }
            } else {
                /* Mixed key types: A string key is considered as larger */
                return p1->key != NULL ? 1 : -1;
            }
            pData2 = &p2->val;
            idx2++;
        } else {
            if (p1->key == NULL) { /* 数字索引 */
                pData2 = zend_hash_index_find(ht2, p1->h);
                if (pData2 == NULL) {
                    return 1;
                }
            } else { /* 字符串索引 */
                pData2 = zend_hash_find(ht2, p1->key);
                if (pData2 == NULL) {
                    return 1;
                }
            }
        }

        pData1 = &p1->val;
        if (Z_TYPE_P(pData1) == IS_INDIRECT) { // 如果变量是间接zval
            pData1 = Z_INDIRECT_P(pData1); // pData1获取它所指向的zval
        }
        if (Z_TYPE_P(pData2) == IS_INDIRECT) { // 如果变量是间接zval
            pData2 = Z_INDIRECT_P(pData2);  // pData2获取它所指向的zval
        }

        if (Z_TYPE_P(pData1) == IS_UNDEF) { 
            if (Z_TYPE_P(pData2) != IS_UNDEF) { // 如果pData1是未定义的变量,而pData2不是未定义的变量,则pData1所在的数组 < pData2所在的数组
                return -1;
            }
        } else if (Z_TYPE_P(pData2) == IS_UNDEF) { // 如果pData1不是未定义的变量,而pData2是未定义的变量,则pData1所在的数组 > pData2所在的数组
            return 1;
        } else {
            result = compar(pData1, pData2); // 如果两者都是不是未定义的变量,则进入compar进行比较
            if (result != 0) {
                return result;
            }
        }
    }

    return 0;
}

以下是手册中,===在面对不同变量的时候运算结果表。

参考


文章来源: http://xz.aliyun.com/t/5426
如有侵权请联系:admin#unsafe.sh