PHP “==” 运算符深究

0x00问题背景

    群友抛出一个题,空数组array()与0 比较大小的结果如何?

    另一个群友也借机抛出了另一个问题

<?php
    var_dump(null == 0);
    var_dump(null == array());
    var_dump(0 == array());

    答案也有些匪夷所思,第一个问题的结果是,array()>0 第二个问题结果是 true,true,false

0x01问题讨论

    实验得到结果后的第一反应就是查文档,于是在php的官方文档中找到如下内容:

    PHP: 比较运算符 - Manual

    PHP: PHP 类型比较表 - Manual

PHP “==” 运算符深究_第1张图片

PHP “==” 运算符深究_第2张图片

    在这里赞一下PHP的文档,描述很详实,demo也有代表性,相信这在语言的推广上起了不少作用。

    问题似乎是解决了,但我还是不满足,颇有点知其然不知其所以然的感觉,于是产生了后面的内容。

0x02进一步探究

    第一个比较初级的想法就是查看opcode,在这里简单提一下opcode。引用官方文档的一段话:

When parsing PHP files, Zend Engine 2 generates a series of operation codes,
commonly known as "opcodes", representing the function of the code.

大意是:Zend引擎在解析PHP文件时,会产生一组操作码,被称作“opcode”,用来表示代码中函数。我觉得描述基本如此,这里就不展开讲了。

于是做了如下实验:

1.建立test文件,内容如下:    

<?php
    var_dump(array() == 0);

2.通过vld扩展查看此段代码的opcode

PHP “==” 运算符深究_第3张图片

从上图的opcode中可以看出除了变量初始化,以及变量返回,并没有对变量比较做更多的展开。关键的内容其实在3行 IS_EQUAL 操作符具体的执行过程当中。于是在PHP文档中找到如下内容PHP: IS_EQUAL - Manual,只有一个关于该操作符的demo,并没有更多的内容。于是只剩下最后一招。

0x03查看源码

    开源就是有这个好处,遇到问题逼不得已还可以看源码,一切也算是掌握之中(从这点上讲IOS的开发者很多情况就只能摸索了)。PHP的源码项目下载可以参见PHP: Git Access,本文参考PHP5.6的源码

    在Zend引擎中,每个opcode根据操作参数的不同(上图中每一行的OP1、OP2)有25个opcode_handler,具体的配置可以参见zend_vm_excute.h 文件                                                                    

static const void *zend_vm_get_opcode_handler(zend_uchar opcode, const 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     */
      };
      return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]];
}

该方法是在Zend引擎中根据opcode 与参数获取opcode handler的方法,可以看到是从zend_opcode_handlers数组中获取的。该数组在 zend_init_opcodes_handlers 方法中初始化,由于数组定义太长(4000行+)就不一一列出了,截取与IS_EQUAL 相关部分:

PHP “==” 运算符深究_第4张图片

具体定位的过程,就将opcode的值带入公式

opcode * 25 + zend_vm_decode[op->op1_type] * 5 + zend_vm_decode[op->op2_type]

当中,opcode的值可以在PHP文档中查到,IS_EQUAL的opcode值为17,当然也可以看到handlers的命名还是很规范的,根据opcode的名字也可以搜索到。根据0x02中的opcode可以看出,两个参数的类型分别为IS_TMP_VAR,IS_CONST。定位到函数ZEND_IS_EQUAL_SPEC_TMPVAR_CONST_HANDLER中

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_IS_EQUAL_SPEC_TMPVAR_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
   USE_OPLINE
   zend_free_op free_op1;
   zval *op1, *op2, *result;

   op1 = _get_zval_ptr_var(opline->op1.var, execute_data, &free_op1);
   op2 = EX_CONSTANT(opline->op2);
   do {
      int result;

      if (EXPECTED(Z_TYPE_P(op1) == IS_LONG)) {
         if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) {
            result = (Z_LVAL_P(op1) == Z_LVAL_P(op2));
         } else if (EXPECTED(Z_TYPE_P(op2) == IS_DOUBLE)) {
            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)) {
            result = (Z_DVAL_P(op1) == Z_DVAL_P(op2));
         } else if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) {
            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)) {
            if (Z_STR_P(op1) == Z_STR_P(op2)) {
               result = 1;
            } else if (Z_STRVAL_P(op1)[0] > '9' || Z_STRVAL_P(op2)[0] > '9') {
               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);
               }
            } else {
               result = (zendi_smart_strcmp(op1, op2) == 0);
            }
            zval_ptr_dtor_nogc(free_op1);

         } 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_TMP_VAR|IS_VAR) == 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);
   zval_ptr_dtor_nogc(free_op1);

   ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

而根据具体的变量类型,IS_ARRAY、IS_LONG走了一圈的if else 发现最后又进了compare_function,该函数的实现可以在zend_operator.c文件中找到。由于函数比较长,switch了各种情况,就不把函数贴上来了。不过在这个函数里最初的两个问题得到了我最满意的解释。

0x04总结

    google是解决问题的利器,好的文档也非常管用。偶尔自己深究一下,乐在其中。

你可能感兴趣的:(PHP,Engine,Zend,opcode)