検索

先直接来看一段展示:

# Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> ceil(-0.5)=> -0.0>>> max(-0.0, 0)=> 0.0>>> max(ceil(-0.5), 0)=> -0.0

上面的演示中,ceil 函数返回的是 -0.0,max 在将 ceil 函数调用的结果作为参数传入的时候,返回的也是一个 -0.0。

如果给 ceil 的结果赋值给变量,还是能得到 -0.0 的结果:

>>> $a = ceil(-0.5)=> -0.0>>> max($a, 0)=> -0.0

下面就来一一分析是哪些原因导致了这些结果的产生。

ceil 会返回 -0.0

首先我们来看一下为什么 ceil 函数会返回 -0.0。

ceil 函数的实现在 $PHP-SRC/ext/stardands/math.c ($PHP-SRC 指的是 PHP 解释器源码根目录)中,为了展示清楚我去掉了一些细节:

PHP_FUNCTION(ceil){    ...    if (Z_TYPE_PP(value) == IS_DOUBLE) {        RETURN_DOUBLE(ceil(Z_DVAL_PP(value)));    } else if (Z_TYPE_PP(value) == IS_LONG) {        convert_to_double_ex(value);        RETURN_DOUBLE(Z_DVAL_PP(value));    }    ...}

从这里可以看出来 ceil 函数做了两个事情:

  • 如果参数类型是 double,则直接调用 C 语言的 ceil 函数并返回执行结果;

  • 如果参数类型是 long,则转换成 double 然后直接返回。

  • 所以 ceil 返回 -0.0 这个本身的原因还在于 C。写个函数测试一下:

    #include <stdio.h>#include <math.h>int main(int argc, char const *argv[]){    printf("%f\n", ceil(-0.5));    return 0;}

    以上代码在我机器上的执行结果是 -0.000000。至于为什么会是这个结果,这是 C 语言的问题,这里也不细说,有兴趣的可以看这里:http://www.wikiwand.com/zh/-0。

    不能直接传入 -0.0

    接下来讨论一下为什么执行 max(-0.0, 0) 却得不到相同的结果。

    用 vld 扩展查看了一下只有以上一行代码的 php 文件看一下结果:

    line     #* E I O op                    fetch      ext  return  operands--------------------------------------------------------------------------   3     0  E >   EXT_STMT         1        EXT_FCALL_BEGIN         2        SEND_VAL                                      0         3        SEND_VAL                                      0         4        DO_FCALL                           2          'max'         5        EXT_FCALL_END   5     6      > RETURN                                        1

    注意到需要为 2 的 SEND_VAL 操作,送进去的值是 0。也就说在词法分析阶段之后 -0.0 就被转换成 0 了。如何转换的呢?下面我们简单的分析一下的过程。

    PHP 的词法分析器由 re2c 生成,语法分析器则是由 Bison 生成。在 zend_language_scanner.l ($PHP-SRC/Zend 目录下)中我们可以找到以下的语句:

    LNUM    [0-9]+DNUM    ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*)EXPONENT_DNUM    (({LNUM}|{DNUM})[eE][+-]?{LNUM})......<ST_IN_SCRIPTING>{DNUM}|{EXPONENT_DNUM} {    zendlval->value.dval = zend_strtod(yytext, NULL);    zendlval->type = IS_DOUBLE;    return T_DNUMBER;}

    LNUM 和 DNUM 后面都是简单的正则表达式。虽然在词法扫描中 0.0 会被标记成 DNUM,并且位于 zend_strtod.c zend_strtod 函数中的也有对于 加减号的处理,但是 - 符号并不和 DNUM 匹配(那既然这样为什么 zend_strtod 还要处理加减号呢?因为这个函数不只是在这里使用的)。这里最终返回一个 T_DNUMBER 的标记。

    再看 zend_language_parser.y 中:

    common_scalar:        T_LNUMBER                     { $$ = $1; }    |    T_DNUMBER                     { $$ = $1; }    ...;static_scalar: /* compile-time evaluated scalars */        common_scalar        { $$ = $1; }    ...    |    '+' static_scalar { ZVAL_LONG(&$1.u.constant, 0); add_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; }    |    '-' static_scalar { ZVAL_LONG(&$1.u.constant, 0); sub_function(&$2.u.constant, &$1.u.constant, &$2.u.constant TSRMLS_CC); $$ = $2; }

    同样我们去掉了一些细节,简单描述一下上面的语法分析的处理流程:

  • T_DNUMBER 是一个 common_scalar 语句;

  • common_scalar 是一个 static_scalar 语句;

  • static_scalar 语句前面存在减号时,将操作数 1 (op1)设定为 值为 0 的 ZVAL_LONG ,然后调用 sub_function 函数处理两个操作数。

  • sub_function 函数的实现位于 zend_operators.c 中,所做的操作很简单,就是用 op1 的值减去 op2 的值,所以就不会存在传入 -0.0 的情况。

    直接调用或赋值给变量

    既然如此,为什么直接使用函数调用做参数或者赋值给变量的方式又可以传入呢?闲来看一下 zend_language_parser.y 中对于函数参数的分析语句:

    function_call_parameter_list:        '(' ')'    { Z_LVAL($$.u.constant) = 0; }    |    '(' non_empty_function_call_parameter_list ')'    { $$ = $2; }    |    '(' yield_expr ')'    { Z_LVAL($$.u.constant) = 1; zend_do_pass_param(&$2, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); };non_empty_function_call_parameter_list:        expr_without_variable    { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }    |    variable                { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }    |    '&' w_variable                 { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }...;

    为了直观 non_empty_function_call_parameter_list 语句块后面我隐去了三行。后面三行的处理逻辑实际上是递归调用,并不影响我们分析。

    通过 function_call_parameter_list 可以看出函数的参数基本情况包括三种:

  • 没有参数

  • 有参数列表

  • 有 yield 表达式

  • 这里我们只需要关注有参数列表的情况,参数列表中的每个参数也分三种情况:

  • 不包含变量的表达式

  • 变量

  • 引用变量

  • 上文中我们提到的直接传入 -0.0 时对应的是第一种情况,传入赋值后的 $a 对应的是第二种情况。参数最终都会交给 zend_do_pass_param 函数(zend_compile.c)去处理。

    那么传入 ceil(-0.5) 作为参数呢?实际上也是对应第二种情况,这个问题单独分析起来也比较复杂,省事儿一点我们直接用 vld 看一下执行 max(ceil(-0.5), 0)过程:

    line     #* E I O op                   fetch       ext  return  operands--------------------------------------------------------------------------   5     0  E >   EXT_STMT         1        EXT_FCALL_BEGIN         2        EXT_FCALL_BEGIN         3        SEND_VAL                                      -0.5         4        DO_FCALL                           1  $0      'ceil'         5        EXT_FCALL_END         6        SEND_VAR_NO_REF                    6          $0         7        SEND_VAL                                      0         8        DO_FCALL                           2          'max'         9        EXT_FCALL_END   6    10      > RETURN                                        1

    序号为 4 的语句中,ceil 的执行结果是赋值给一个 $0 的变量,而在序号为 6 的执行中,执行的是 SEND_VAR_NO_REF 的语句,调用的 $0。SEND_VAR_NO_REF 的 Opcode 是在何时被指定的呢?也是在 zend_do_pass_param 函数中:

    if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) {    /* Method call */    op = ZEND_SEND_VAR_NO_REF;    ...}

    函数执行过程中使用 zend_parse_parameters 函数(zend_API.c)来获取参数。从参数的存储到获取中间还有很多处理过程,这里不再一一详解。但是需要知道一件事:函数在使用变量作为参数的时候是直接从已经存储的变量列表中读取的,没有经过过滤处理,所以变量 $a 或 ceil(-0.5) 才可以直接将 -0.0 传递给 max 函数使用。

    最后的原因

    既然以上都知道了,那还剩一个问题:为什么在 -0.0 和 0 中 max 函数会选择前者?

    其实这个问题很简单,看一下 max 函数的实现($PHP-SRC/ext/standard/array.c)就知道真的就是在两值相等时选择了前者:

    max = args[0];for (i = 1; i < argc; i++) {    is_smaller_or_equal_function(&result, *args[i], *max TSRMLS_CC);    if (Z_LVAL(result) == 0) {        max = args[i];    }}

    同样,min 函数也存在这个问题,区别就是 min 函数是调用的 is_smaller_function 来比较两个数值,两个值相等的时候返回前者。

    所以要解决这个问题也很简单,只需要调换一下参数顺序即可:

    # Psy Shell v0.3.3 (PHP 5.5.30 — cli) by Justin Hileman>>> max(0, ceil(-0.5))=> 0

    后话

    本文仅仅是管中窥豹,从一个小 “bug” 入口简单的梳理一下各个环节的处理过程,如果想要更深入的理解 PHP 的执行过程,还需要大量的精力和知识储备。

    分析 PHP 源码的执行过程不仅是为了对 PHP 有更深刻的理解,也能帮助我们了解一门语言从代码到执行结果中间的各个环节和实现。

    关于词法分析器与语法分析器,这里讲的并不多,希望后面有机会的话能够再深入探讨。re2c 的规则比较简单,关于 Bison,则有很多相关的书籍。

    文中有粗浅的疏解,也留下有问题,如有错误,欢迎指正。

    Stay foolish,stay humble; Keep questioning,keep learning.

    私博地址:http://0x1.im

    声明
    この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
    誇大広告を超えて:今日のPHPの役割の評価誇大広告を超えて:今日のPHPの役割の評価Apr 12, 2025 am 12:17 AM

    PHPは、特にWeb開発の分野で、最新のプログラミングで強力で広く使用されているツールのままです。 1)PHPは使いやすく、データベースとシームレスに統合されており、多くの開発者にとって最初の選択肢です。 2)動的コンテンツ生成とオブジェクト指向プログラミングをサポートし、Webサイトを迅速に作成および保守するのに適しています。 3)PHPのパフォーマンスは、データベースクエリをキャッシュおよび最適化することで改善でき、その広範なコミュニティと豊富なエコシステムにより、今日のテクノロジースタックでは依然として重要になります。

    PHPの弱い参照は何ですか、そしていつ有用ですか?PHPの弱い参照は何ですか、そしていつ有用ですか?Apr 12, 2025 am 12:13 AM

    PHPでは、弱い参照クラスを通じて弱い参照が実装され、ガベージコレクターがオブジェクトの回収を妨げません。弱い参照は、キャッシュシステムやイベントリスナーなどのシナリオに適しています。オブジェクトの生存を保証することはできず、ごみ収集が遅れる可能性があることに注意する必要があります。

    PHPで__invoke Magicメソッドを説明してください。PHPで__invoke Magicメソッドを説明してください。Apr 12, 2025 am 12:07 AM

    \ _ \ _ Invokeメソッドを使用すると、オブジェクトを関数のように呼び出すことができます。 1。オブジェクトを呼び出すことができるように\ _ \ _呼び出しメソッドを定義します。 2。$ obj(...)構文を使用すると、PHPは\ _ \ _ Invokeメソッドを実行します。 3。ロギングや計算機、コードの柔軟性の向上、読みやすさなどのシナリオに適しています。

    同時性については、PHP 8.1の繊維を説明します。同時性については、PHP 8.1の繊維を説明します。Apr 12, 2025 am 12:05 AM

    繊維はPhp8.1で導入され、同時処理機能が改善されました。 1)繊維は、コルーチンと同様の軽量の並行性モデルです。 2)開発者がタスクの実行フローを手動で制御できるようにし、I/O集約型タスクの処理に適しています。 3)繊維を使用すると、より効率的で応答性の高いコードを書き込むことができます。

    PHPコミュニティ:リソース、サポート、開発PHPコミュニティ:リソース、サポート、開発Apr 12, 2025 am 12:04 AM

    PHPコミュニティは、開発者の成長を支援するための豊富なリソースとサポートを提供します。 1)リソースには、公式のドキュメント、チュートリアル、ブログ、LaravelやSymfonyなどのオープンソースプロジェクトが含まれます。 2)StackOverFlow、Reddit、およびSlackチャネルを通じてサポートを取得できます。 3)開発動向は、RFCに従うことで学ぶことができます。 4)コミュニティへの統合は、積極的な参加、コード共有への貢献、および学習共有への貢献を通じて達成できます。

    PHP対Python:違いを理解しますPHP対Python:違いを理解しますApr 11, 2025 am 12:15 AM

    PHP and Python each have their own advantages, and the choice should be based on project requirements. 1.PHPは、シンプルな構文と高い実行効率を備えたWeb開発に適しています。 2。Pythonは、簡潔な構文とリッチライブラリを備えたデータサイエンスと機械学習に適しています。

    PHP:それは死にかけていますか、それとも単に適応していますか?PHP:それは死にかけていますか、それとも単に適応していますか?Apr 11, 2025 am 12:13 AM

    PHPは死にかけていませんが、常に適応して進化しています。 1)PHPは、1994年以来、新しいテクノロジーの傾向に適応するために複数のバージョンの反復を受けています。 2)現在、電子商取引、コンテンツ管理システム、その他の分野で広く使用されています。 3)PHP8は、パフォーマンスと近代化を改善するために、JITコンパイラおよびその他の機能を導入します。 4)Opcacheを使用してPSR-12標準に従って、パフォーマンスとコードの品質を最適化します。

    PHPの未来:適応と革新PHPの未来:適応と革新Apr 11, 2025 am 12:01 AM

    PHPの将来は、新しいテクノロジーの傾向に適応し、革新的な機能を導入することで達成されます。1)クラウドコンピューティング、コンテナ化、マイクロサービスアーキテクチャに適応し、DockerとKubernetesをサポートします。 2)パフォーマンスとデータ処理の効率を改善するために、JITコンパイラと列挙タイプを導入します。 3)パフォーマンスを継続的に最適化し、ベストプラクティスを促進します。

    See all articles

    ホットAIツール

    Undresser.AI Undress

    Undresser.AI Undress

    リアルなヌード写真を作成する AI 搭載アプリ

    AI Clothes Remover

    AI Clothes Remover

    写真から衣服を削除するオンライン AI ツール。

    Undress AI Tool

    Undress AI Tool

    脱衣画像を無料で

    Clothoff.io

    Clothoff.io

    AI衣類リムーバー

    AI Hentai Generator

    AI Hentai Generator

    AIヘンタイを無料で生成します。

    ホットツール

    AtomエディタMac版ダウンロード

    AtomエディタMac版ダウンロード

    最も人気のあるオープンソースエディター

    MantisBT

    MantisBT

    Mantis は、製品の欠陥追跡を支援するために設計された、導入が簡単な Web ベースの欠陥追跡ツールです。 PHP、MySQL、Web サーバーが必要です。デモおよびホスティング サービスをチェックしてください。

    ZendStudio 13.5.1 Mac

    ZendStudio 13.5.1 Mac

    強力な PHP 統合開発環境

    EditPlus 中国語クラック版

    EditPlus 中国語クラック版

    サイズが小さく、構文の強調表示、コード プロンプト機能はサポートされていません

    SecLists

    SecLists

    SecLists は、セキュリティ テスターの究極の相棒です。これは、セキュリティ評価中に頻繁に使用されるさまざまな種類のリストを 1 か所にまとめたものです。 SecLists は、セキュリティ テスターが必要とする可能性のあるすべてのリストを便利に提供することで、セキュリティ テストをより効率的かつ生産的にするのに役立ちます。リストの種類には、ユーザー名、パスワード、URL、ファジング ペイロード、機密データ パターン、Web シェルなどが含まれます。テスターはこのリポジトリを新しいテスト マシンにプルするだけで、必要なあらゆる種類のリストにアクセスできるようになります。