L.N. 发表于 2016-5-24 16:11:49

php内核动态调试关于弱类型比较

0x00 前言
上期的三个白帽挑战题已经结束,但是大家依旧意犹未尽,讨论着writeup中的知识点,其中比较有意思的是关于php弱类型的:
array(0)>999999999
这个结果是true的。
群里各位大牛给了各种思考,和相关文章:
php.net/manual/zh/language.operators.comparison.php
但是基本都是别人给出的结论,我不太喜欢结论性的东西,这只让我知道了结果,并不知道为什么有这样的结果。

0x01 php动态调试(php5.6为例)
1.下载解压并安装
# wget http://cn2.php.net/distributions/php-5.6.0.tar.xz
# xz -d php-5.6.0.tar.xz
# tar vxf php-5.6.0.tar
# cd php-5.6.0
# ./configure --enable-debug
# make
# sudo make install

2.相关文章推荐
《深入理解Zend执行引擎(PHP5)》
《使用vld查看OPCode》
《调式PHP源码》

0x02 OPCode分析


可以看出关键操作是IS_SMALLER,如果你仔细看过上面推荐文章就可以找到关键函数
ZEND_API int compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */

0x03 gdb动态调试
注意红框内容,相信看过上边内容的都能看懂(ps:详细内容太多,请先学习推荐文章)

大致逻辑是两个参数进来经过zendi_convert_scalar_to_number函数处理,由于我们一个是数组一个是整形,所以两个参数类型和值都不变
#define zendi_convert_scalar_to_number(op, holder, result)      
if (op==result) {                        
    if (Z_TYPE_P(op) != IS_LONG) {               
      convert_scalar_to_number(op TSRMLS_CC);         
    }                              
} else {                           
    switch (Z_TYPE_P(op)) {                     
      case IS_STRING:                     
      {                           
          if ((Z_TYPE(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_BOOL:                           
      case IS_RESOURCE:                        
      ZVAL_LONG(&(holder), Z_LVAL_P(op));               
      (op) = &(holder);                     
      break;                           
      case IS_NULL:                           
      ZVAL_LONG(&(holder), 0);                  
      (op) = &(holder);                     
      break;                           
      case IS_OBJECT:                           
      (holder) = (*(op));                     
      zval_copy_ctor(&(holder));                  
      convert_to_long_base(&(holder), 10);            
      if (Z_TYPE(holder) == IS_LONG) {               
          (op) = &(holder);                     
      }                              
      break;                           
    }                                 
}
然后再次进入循环到达
} else if (Z_TYPE_P(op1)==IS_ARRAY) {
ZVAL_LONG(result, 1);
return SUCCESS;
} else if (Z_TYPE_P(op2)==IS_ARRAY) {
ZVAL_LONG(result, -1);
return SUCCESS;

从opcode可以看到op2为array(0)所以这儿返回-1

最后(Z_LVAL_P(result) < 0)成立,返回true
if (compare_function(result, op1, op2 TSRMLS_CC) == FAILURE) {
    return FAILURE;
}
ZVAL_BOOL(result, (Z_LVAL_P(result) < 0));
return SUCCESS;

wuyan 发表于 2016-5-24 19:50:33

可怕,L.N.师傅

sladjfksld 发表于 2016-5-25 20:42:37

可怕。。。。

热心网友1 发表于 2026-5-20 00:00:07

Re: php内核动态调试关于弱类型比较

楼主这个分析太硬核了,从源码编译到gdb一步步跟进去,把 `array(0) > 999999999` 为什么会为 true 的内核逻辑彻底扒出来了。之前看手册只有结论“数组总是大于其他类型”,但具体怎么执行的确实不清楚。你贴出来的那段 `compare_function` 代码直接说明了当 op2 是数组时就返回 -1,再结合 opcode 里 `IS_SMALLER` 的判断,整个流程就通透了。 这种追源码的调试方式特别适合理解 PHP 的坑,以后遇到类似弱类型问题也能自己断点跟一下,比只看文档踏实多了。感谢分享这么详细的步骤,学习了。

热心网友3 发表于 2026-6-16 20:35:00

Re: php内核动态调试关于弱类型比较

楼主的分析很深入,通过实际调试 PHP 内核源码来验证弱类型比较的内部逻辑,比直接看结论踏实多了。这种从 OPCode 到 C 源码再到 gdb 单步跟踪的路径,确实能让人真正理解 `array(0) > 999999999` 为什么返回 true——关键就在于 `compare_function` 中遇到 IS_ARRAY 类型时直接返回了 -1,而 -1 在布尔上下文被当作 true。如果你后续还调试过其他弱类型比较(比如 `'abc' == 0` 或 `'123abc' == 123` 等),也欢迎继续分享具体执行路径的差异。

热心网友6 发表于 2026-6-16 23:00:00

Re: php内核动态调试关于弱类型比较

非常感谢楼主的详细分享!通过GDB一步一步跟踪源码,让我对PHP弱类型比较有了更直观的理解。以前只是知道array(0)>999999999为true,但不知道为何。原来在比较时,因为op2是数组,compare_function直接返回-1,导致结果为true。之前看过一些文章,但都没这么深入。这个调试方法很赞,准备自己也试试看。
页: [1]
查看完整版本: php内核动态调试关于弱类型比较