【译】Equals之opcode分析

原文: https://blog.ircmaxell.com/2012/07/the-anatomy-of-equals-opcode-analysis.html

昨天有人通过电子邮件问我一个有趣的问题,问题相当简单,答案,不多……所以,与其用电子邮件回复,我想我应该写一篇关于它的文章。简单地说,问题是:当使用==将浮点数与整数进行比较时,在哪里进行转换?

那么,让我们开始:

测试代码:

在回答问题之前,我们需要做的第一步是定义我们可以分析流程的最简单代码。因此,我们可以从以下代码开始:

1
<?php 1 == 1.0;

但这有点太简单了,我们想知道变量会发生什么,但是没有变量!!!!因此,让我们添加一些变量:

1
2
3
4
<?php
$i = 1;
$j = 1.0;
echo $i == $j;

现在我们状态很好,所以,下一步就是搞清楚到底发生了什么。
最简单的方法是查看生成的操作码,我用了5.4.4版本的Vulcan逻辑分解器。


The Opcodes

这4行PHP生成这8行操作码:

1
2
3
4
5
6
7
8
9
10
line     # *  op           fetch  ext  return  operands
--------------------------------------------------------
3 0 > EXT_STMT
1 ASSIGN !0, 1
4 2 EXT_STMT
3 ASSIGN !1, 1
5 4 EXT_STMT
5 IS_EQUAL ~2 !0, !1
6 ECHO ~2
6 7 > RETURN 1

现在,为了我们的目的,我们对操作码第5行感兴趣,这是一个等价的调用。
但在深入研究之前,让我们先谈谈我们已经提供的输出。

第一点有趣的信息在“op”列中,这告诉我们将要执行的操作,然后,右边有一堆数字。
前缀(~和!这里)是变量,其他是原始数值。~和!是吗?他们是一个编译变量(普通的PHP变量),其中~表示一个临时变量(直接将值从一个操作码传递到另一个操作码时使用)。

所以,在操作码5之前,我们刚刚将这两个值赋给了这两个变量(值得注意的是!1-因此$J-是一个浮点数,这里不输出小数)。因此,当我们到达第5行时,我们已经得到了所有需要的信息,以确定“等于”是什么。


Opcode Handlers

在我们深入研究is_equal之前,我们首先需要讨论操作码处理程序。当PHP生成它的VM(是的,它是生成的)时,它可以使用每个操作码的多个“版本”。对于is_equal,如果操作数是常量(1和1.0),而不是变量(变量处理代码需要不同),则可以想象获取参数所需的代码是不同的。

因此,处理程序是在zend/zend_vm_execute.h中定义的。它们具有命名约定:ZEND_{$OPCODE}_SPEC_{$VAR1_TYPE}_{$VAR2_TYPE}_HANDLER 。因此,通过观察IS_EQUALS 的调用,我们可以知道它们都是编译变量。因此,我们寻找 ZEND_IS_EQUALS_SPEC_CV_CV_HANDLER 的定义。果然,在第34805行。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int ZEND_FASTCALL  ZEND_IS_EQUAL_SPEC_CV_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE

zval *result = &EX_T(opline->result.var).tmp_var;

SAVE_OPLINE();
ZVAL_BOOL(result, fast_equal_function(result,
_get_zval_ptr_cv_BP_VAR_R(EX_CVs(), opline->op1.var TSRMLS_CC),
_get_zval_ptr_cv_BP_VAR_R(EX_CVs(), opline->op2.var TSRMLS_CC) TSRMLS_CC));


CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}

让我们忽略所有代码, 只看fast_equal_function。这些论点是非常直截了当的,除了时髦的“get_zval_ptr_blah_blah”行。这些都是获取变量值(zval,记得吗?)从操作码数组。然后,我们只需要调用fast_equal_function(result,var1,var2)。


Fast Equal Function

继续,我们需要查看fast_equal_function的内部,看看发生了什么。值得注意的是,到目前为止我们还没有修改任何变量。所以!0($i)和!1($j)仍然分别是一个整数和一个浮点。那么让我们来看看相等函数吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static zend_always_inline int fast_equal_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
{
if (EXPECTED(Z_TYPE_P(op1) == IS_LONG)) {
if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) {
return Z_LVAL_P(op1) == Z_LVAL_P(op2);
} else if (EXPECTED(Z_TYPE_P(op2) == IS_DOUBLE)) {
return ((double)Z_LVAL_P(op1)) == Z_DVAL_P(op2);
}
} else if (EXPECTED(Z_TYPE_P(op1) == IS_DOUBLE)) {
if (EXPECTED(Z_TYPE_P(op2) == IS_DOUBLE)) {
return Z_DVAL_P(op1) == Z_DVAL_P(op2);
} else if (EXPECTED(Z_TYPE_P(op2) == IS_LONG)) {
return Z_DVAL_P(op1) == ((double)Z_LVAL_P(op2));
}
}
compare_function(result, op1, op2 TSRMLS_CC);
return Z_LVAL_P(result) == 0;
}

看起来里面发生了很多事情,但实际上,这很简单。
值得注意的一点是,expected()只是一个宏包装器,出于我们的目的,我们可以将它忽略。

所以,在函数里面,我们有3个分支。

  1. 第一个是如果第一个变量是整数。
  2. 第二个是如果第一个变量是一个浮点。(double也是浮点型)
  3. 如果第一个变量是其他变量,则会出现第三个分支和最后一个分支。

在前两个分支中的每一个,都有另外两个分支检查第二个参数是整数还是浮点。
如果是这样,它将进行简单的数字比较,如果不是,则调用下面的通用比较函数compare_function。

因此这就是我们的答案:

$I==$J 根本没有ZVAL强制转换,它只执行简单的C变量数值转换(基于值,不需要额外的内存分配)。
所以两个源变量都保持不变,没有进行“转换”。


结论

如果你知道该去哪里看的话,阅读资料并不难。试一试。有关练习,请查看[compare_function()](http://lxr.php.net/xref/php_5_4/zend/zend_operators.c 1402),并了解如果一个参数是字符串“2abc”,另一个是整数2,会发生什么情况…

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!