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

本文首发先知社区

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

前言

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 常规数据类型 */
#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 分析过程

测试代码

1
2
3
4
<?php
$a = "123";
var_dump($a == 123);
var_dump($a === 123);

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

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

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

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

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

路径:Zend/zend_language_scanner.l:1476

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

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

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

路径:Zend/zend_language_parser.y:935

1
2
|	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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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中变量对比的一个核心函数,

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

1
2
3
4
5
6
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
			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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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;
}

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

参考