PHP7 核心 — opcode 結構詳解

c9s
PHP Hacks
Published in
9 min readApr 4, 2016

PHP7 與 PHP5 之最大的不同,在於核心結構的 zval 大改版,連帶的 zval 取值所有 macro 也都一起改變了,以至於 PHP5 可使用的 Extension 需整個改寫,才可以在 PHP7 上正確執行,而無法單純只使用 C Macro 來向後相容。

筆者最近在研究 php7 的 getter method 優化,因此稍微更進一步的研究了 Zend Engine 的運作與修改

PHP 腳本的執行很簡單,就是剖析、編譯、執行。 編譯,就是從剖析出來的 AST 產生 Opcode,執行,就是從 Opcode 來解譯命令。

php 的原碼結構中,目錄 Zend/ 為 Zend Engine 的核心程式,其 zend_execute.* 為執行階段相關代碼、zend_compile.* 為編譯階段代碼、zend_ast.* 為剖析階段代碼。

Zend Op Array — Zend Engine 中的編譯單位

一段簡單的 PHP 腳本,經過 Joe Watkins 開發的 explain 工具,顯示出來的 Opcode 大概是這樣:

PHP 核心中,儲存 opcode 的容器是 zend_op_array struct,這個結構中被共用於各種不同的編譯單位,如: file, function, method, closure .. 等等。

舉例來說,在執行 Function 時,執行的是 Function 的 zend_op_array,在執行腳本時,執行的是 file 的 zend_op_array。

上圖中,可以看到有許多 ZEND_EXT_* 開頭的 opcode,這些是 compiler_options 的 extended_info 被啟用時,xdebug 或 xhprof 等 extension 可用以計算執行時間所使用的 opcode,若無使用 xdebug, xhprof 等 zend_extension,則不會出現這些 opcodes。

zend_compile.h 標頭檔中定義的 op_array 結構如下:

由於結構大,所以這邊就只先顯示最主要的幾種。

其中:

  1. type 定義了這個 op_array 的類型,可以是 ZEND_INTERNAL_FUNCTION (zend extension 中定義函式)、ZEND_USER_FUNCTION (使用者定義的函式)
  2. arg_flags 為定義參數相關資訊,譬如: 是否為 pass_by_reference
  3. fn_flags 為 op_array 為 function 時,定義函式的屬性,譬如 Public, Protected, Abstract … 等等
  4. function_name 為 op_array 為 function 時,定義該函式名稱。
  5. scope 在不同的語境下有不同的意義,在 method 中,scope 就是該 method 所屬 class 的 zend_class_entry,zend_class_entry 定義了類別所有的資訊。
  6. this_var 則為當下的 instance 變數,該變數使用 uint32 來表示主要是盡可能的讓 op_array 夠緊實~ 在執行期間需要透過 EX_VAR() 巨集,來取得實際的 zval 結構。
  7. num_args 為函式的參數數量。
  8. required_num_args 為函式的最小參數數量。
  9. literals 這個欄位指向一個 zval 型別的記憶體位址,存放的是剖析時期的資訊,譬如: variable names, constant names … 等等。

Zend Op — 運算子及運算元

一個 zend_op_array 又可包含許多個 opcodes,每個 opcode 都是被存在一個 zend_op_array 中型別為 zend_op* 的 opcodes 陣列中,在 zend_compile.c 或 zend_opcodes.c 中,每個 opcode 的單位通常又被叫做 opline。

每一個型別為 zend_op* 的 opcode 的結構都相當簡單,zend_compile.h 所定義的 zend_op struct 如下:

_zend_op struct

雖然看起來很多欄位,但最主要只有四種類型,一種是定義要做什麼 (opcode, handler) ,運算子1 operand (op1, op1_type) ,運算子2 operand (op2, op2_type) 以及運算結果 (result, result_type)

opcode 欄位,型別為 zend_uchar,這是一個常數,通常 opcode 可使用下方語法判斷:

opcode const condition

如果 opcode 的資訊不夠使用,extended_value 這個欄位會在編譯時期,給予更多資訊,讓解譯器在執行的時候,可以分辨不同的條件。

以加法運算來說,opcode 是 ZEND_ADD,然後再依照運算子的類型,有不同的處理函式 (Handler)

運算子的類型大概有以下幾種:

  1. IS_CONST: 是指常數,不會在動態時期被修改的數值,如數字 123, 字串 “string” 或者浮點數: 1.234 等等,這邊所說的常數,不是指變數內的常數,而是如 $foo->getNumber() 其中的 method call “getNumber” 就是 IS_CONST,又或者 $a + 3 ,其中的 3 也是 IS_CONST
  2. IS_CV: 是指 PHP 腳本中所定義的變數,如: $var, $foo, $bar 等等。在 opcode 表示法中,通常以 “!” 表示。
  3. IS_TMP_VAR: 是指 PHP 腳本中未定義,但在運算過程中,需要暫時儲存的變數,通常為 non-assignment 且 non-terminal 的表達式使用,這些變數不會被共用,也因此不被 reference counting 保護。在 opcode 表示法中,通常以 “~” 符號表示。
  4. IS_VAR: 通常是在 ZEND_FETCH_(R|W|RW) 或者有 assignment ,通常是跟真實的變數緊密連結,也因此 reference counting 必須被考慮。 在 opcode 表示法中,通常以 “$” 符號表示。
  5. IS_UNUSED: 代表在這個 opline 中,這個 operand 沒有被使用到。

op_array 與 zend_op 結構中,不會儲存實際執行用的變數,這兩個結構只會儲存剖析完的資料,以及變數在變數陣列中的 array index。也因此,如果你使用 vld 這類的 extension 來將 oplines 印出來,你會發現 op1 可能是 $1, ~3 之類的代號,而不是實際的數據。

舉例來說,剖析完的語意相關資料,會被儲存在 op_array 裡頭的 literals field,要存取變數名稱、常數名稱都是使用 index 去存取,譬如:

執行時期可使用 EX_LITERALS() 來取得 literals,而編譯的時候,Zend Engine 透過如下代碼來對 Constant 做查找快取:

Z_CACHE_SLOT(op_array->literals[opline->op2.constant]) = -1

Opcode Handler — Opcode 處理函式

由於 opcode 可對應不同種類的運算子,因此在 opcode handler 中也要針對不同的運算子處理。 zend_vm_execute.h 這個標頭檔,定義了所有 opcode 對應的處理函式。

舉例來說 ZEND_ADD 可能會搭配 CV , CV 或 CV, CONST 做運算,在 zend_vm_execute.h 裡面,就會有 …ZEND_ADD_SPEC_CV_CV.. 這樣的函式做對應。

而為了要讓 opcode handler 的處理邏輯可以寫在一起,方便開發維護,因此 zend_vm_execute.h 是透過以 PHP 撰寫的 Zend/zend_gen_vm.php 程式,從 zend_vm_def.h 標頭檔產生出來的。

也因此,只要修改了 zend_vm_def.h ,就得執行一次 php Zend/zend_gen_vm.php 將 Zend/zend_vm_execute.h 做更新。

目前在 Zend Engine 所定義的 opcode 大約有 184 種,可參見 Zend/zend_vm_opcodes.h 標頭檔所定義的 opcode 對應的 constants:

zend_vm_opcodes.h

編譯時期 (Compile-Time) 執行時期 (Run-time) 所調用的巨集

Zend Engine 中,不同時期所調用的巨集名稱,基本上都是有規則的。 譬如

  1. RT — Run-Time
  2. CT — Compile-time 或 Constant-time
  3. EX — 目前的 execution_data
  4. EG — Executor Globals
  5. CG — Compiler Globals

這些定義都可以在 Zend/zend_compile.h , Zend/zend_execute.h, Zend/zend_globals_macros.h 裡找到。

而在 zend_op 結構內

可以看到 ZEND_USE_ABS_CONST_ADDR 這個巨集檢查,當 PHP 是在 32 位元的機器上編譯時,使用 absolute address 做 jump targets,若在 64 位元,則使用 32bit 的 relative offset 做計算。

也因此,新增的 EX_CONTANTS() 巨集,通過不同的編譯條件,讓 VM Handler 可以更快的取到數值。 而在編譯時期,則採用 CT_CONSTANT() 巨集來取得數據。

關於執行時期的解析,下集待續。

--

--

c9s
PHP Hacks

Yo-an Lin, yet another programmer in 21 century