PHP黑魔法深度剖析(一)——PHP的弱类型比较

2019-06-19 约 2054 字 预计阅读 10 分钟

声明:本文 【PHP黑魔法深度剖析(一)——PHP的弱类型比较】 由作者 MengChen 于 2019-06-19 09:32:00 首发 先知社区 曾经 浏览数 50 次

感谢 MengChen 的辛苦付出!

最近一直在学习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) /* 222{ */
{
    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;
}

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

参考

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now