摘要:的詞法分析器由生成,語法分析器則是由生成。這里最終返回一個的標記。關于詞法分析器與語法分析器,這里講的并不多,希望后面有機會的話能夠再深入探討。
先直接來看一段展示:
# 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#include int main(int argc, char const *argv[]) { printf("%f ", 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}) ... ...{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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/21210.html
摘要:記錄下整體的設計思路以及運營過程中的各種問題。如果錢是負數了,還得從已生成的小紅包中抽取回來將紅包放入隊列之中創建紅包失敗,請檢查參數生產和之間的隨機數,但是概率不是平均的,從到方向概率逐漸加大。 公司前段時間根據業務方需求需要做一個搶紅包的活動,網上也搜索了很多資料。記錄下整體的設計思路以及運營過程中的各種問題。 產品需求: 1.紅包支持配置開始時間、結束時間、類型(隨機金額或固定金...
摘要:記錄下整體的設計思路以及運營過程中的各種問題。如果錢是負數了,還得從已生成的小紅包中抽取回來將紅包放入隊列之中創建紅包失敗,請檢查參數生產和之間的隨機數,但是概率不是平均的,從到方向概率逐漸加大。 公司前段時間根據業務方需求需要做一個搶紅包的活動,網上也搜索了很多資料。記錄下整體的設計思路以及運營過程中的各種問題。 產品需求: 1.紅包支持配置開始時間、結束時間、類型(隨機金額或固定金...
摘要:記錄下整體的設計思路以及運營過程中的各種問題。如果錢是負數了,還得從已生成的小紅包中抽取回來將紅包放入隊列之中創建紅包失敗,請檢查參數生產和之間的隨機數,但是概率不是平均的,從到方向概率逐漸加大。 公司前段時間根據業務方需求需要做一個搶紅包的活動,網上也搜索了很多資料。記錄下整體的設計思路以及運營過程中的各種問題。 產品需求: 1.紅包支持配置開始時間、結束時間、類型(隨機金額或固定金...
摘要:等平臺平臺由于我開發以為主,所以就用的環境配置來學習。啟動進程的用戶和用戶組,進程運行的用戶必須要設置。模式模式,表示啟動進程是動態分配的,隨著請求量動態變化的。 centos等linux平臺 /usr/local/php/php /usr/local/php/etc/php.ini /usr/local/php/sbin/php-fpm /usr/local/php/etc/php-...
閱讀 1184·2021-11-22 13:54
閱讀 2435·2021-09-22 15:36
閱讀 2738·2019-08-30 15:54
閱讀 809·2019-08-30 15:53
閱讀 3172·2019-08-30 15:53
閱讀 518·2019-08-29 15:21
閱讀 2870·2019-08-28 18:28
閱讀 3015·2019-08-26 13:37