PHP核心分析-Zend虛擬機器詳解
PHP 是一門解釋型的語言。諸如Java、Python、Ruby、Javascript 等解釋型語言,我們寫的程式碼不會被編譯成機器碼運行,而是會被編譯中間碼運行在虛擬機器( VM)上。運行 PHP 的虛擬機,稱為 Zend 虛擬機,今天我們將深入內核,探討 Zend 虛擬機運作的原則。
OPCODE
什麼是 OPCODE?它是一種虛擬機器能夠識別並處理的指令。 Zend 虛擬機器包含了一系列的OPCODE,透過OPCODE 虛擬機器能夠做很多事情,列舉幾個OPCODE 的例子:
ZEND_ADD
將兩個運算元相加。ZEND_NEW
建立一個 PHP 物件。ZEND_ECHO
將內容輸出到標準輸出。ZEND_EXIT
退出 PHP。
諸如此類的操作,PHP 定義了186個(隨著PHP 的更新,肯定會支援更多種類的OPCODE),所有的OPCODE 的定義和實作都可以在原始碼的zend/zend_vm_def.h
檔案(這個檔案的內容不是原生的C 程式碼,而是一個模板,後面會說明原因)中查閱到。
我們來看下 PHP 是如何設計 OPCODE 資料結構:
struct _zend_op { const void *handler; znode_op op1; znode_op op2; znode_op result; uint32_t extended_value; uint32_t lineno; zend_uchar opcode; zend_uchar op1_type; zend_uchar op2_type; zend_uchar result_type; };
仔細觀察 OPCODE 的資料結構,是不是能找到組合語言的感覺。每一個OPCODE 都包含兩個運算元,op1
和 op2
,handler
指標則指向了執行該OPCODE 運算的函數,函數處理後的結果,會保存在 result
中。
我們舉一個簡單的例子:
<?php $b = 1; $a = $b + 2;
我們透過 vld 擴充功能看到,經過編譯的後,上面的程式碼產生了 ZEND_ADD 指令的 OPCODE。
compiled vars: !0 = $b, !1 = $a line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0, 1 3 1 ADD ~3 !0, 2 2 ASSIGN !1, ~3 8 3 > RETURN 1
其中,第二行是 ZEND_ADD
指令的 OPCODE。我們看到,它接收2個運算元,op1
是變數 $b
,op2
是數位常數1,回傳的結果存入了臨時變數。在 zend/zend_vm_def.h
檔案中,我們可以找到 ZEND_ADD 指令對應的函數實作:
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV) { USE_OPLINE zend_free_op free_op1, free_op2; zval *op1, *op2, *result; op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R); op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R); if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) { if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) { result = EX_VAR(opline->result.var); fast_long_add_function(result, op1, op2); ZEND_VM_NEXT_OPCODE(); } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) { result = EX_VAR(opline->result.var); ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2)); ZEND_VM_NEXT_OPCODE(); } } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) { ... }
上面的程式碼不是原生的 C 程式碼,而是一種範本。
為什麼要這樣做?因為 PHP 是弱型別語言,而其實作的 C 則是強型別語言。弱型別語言支援自動型別匹配,而自動型別匹配的實作方式,就像上述程式碼一樣,透過判斷來處理不同型別的參數。試想一下,如果每一個 OPCODE 處理的時候都需要判斷傳入的參數類型,那麼性能勢必成為極大的問題(一次請求需要處理的 OPCODE 可能能達到成千上萬個)。
哪有辦法嗎?我們發現在編譯的時候,已經能夠確定每個操作數的類型(可能是常數還是變數)。所以,PHP 真正執行時的 C 程式碼,不同型別運算元會分成不同的函數,讓虛擬機器直接呼叫。這部分程式碼放在了 zend/zend_vm_execute.h
中,展開後的檔案相當大,而且我們注意到還有這樣的程式碼:
if (IS_CONST == IS_CV) {
完全沒有什麼意義是吧?不過沒有關係,C 的編譯器會自動最佳化這樣判斷。大多數情況,我們希望了解某個 OPCODE 處理的邏輯,還是透過閱讀範本檔案 zend/zend_vm_def.h
比較容易。順便說一下,根據模板產生 C 程式碼的程式就是用 PHP 實現的。
執行過程
準確的來說,PHP 的執行分成了兩大部分:編譯和執行。這裡我將不會詳細展開編譯的部分,而是把焦點放在執行的過程。
經過語法、詞法分析等一系列的編譯過程後,我們得到了一個名為 OPArray 的數據,其結構如下:
struct _zend_op_array { /* Common elements */ zend_uchar type; zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string *function_name; zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_arg_info *arg_info; /* END of common elements */ uint32_t *refcount; uint32_t last; zend_op *opcodes; int last_var; uint32_t T; zend_string **vars; int last_live_range; int last_try_catch; zend_live_range *live_range; zend_try_catch_element *try_catch_array; /* static variables support */ HashTable *static_variables; zend_string *filename; uint32_t line_start; uint32_t line_end; zend_string *doc_comment; uint32_t early_binding; /* the linked list of delayed declarations */ int last_literal; zval *literals; int cache_size; void **run_time_cache; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; };
內容超多對吧?簡單的理解,其本質就是一個 OPCODE 數組外加執行過程中所需的環境資料的集合。介紹幾個相對來說比較重要的欄位:
opcodes
存放 OPCODE 的陣列。filename
目前執行的腳本的檔案名稱。function_name
目前執行的方法名稱。static_variables
靜態變數清單。last_try_catch
try_catch_array
目前上下文中,如果出現例外 try-catch-finally 跳轉所需的資訊。literals
所有諸如字串 foo 或數字23,這樣的常數字面量集合。
為什麼需要產生這樣龐大的資料?因為編譯時期產生的資訊越多,執行時期所需的時間就越少。
接下来,我们看下 PHP 是如何执行 OPCODE。OPCODE 的执行被放在一个大循环中,这个循环位于 zend/zend_vm_execute.h
中的 execute_ex
函数:
ZEND_API void execute_ex(zend_execute_data *ex) { DCL_OPLINE zend_execute_data *execute_data = ex; LOAD_OPLINE(); ZEND_VM_LOOP_INTERRUPT_CHECK(); while (1) { if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) { if (EXPECTED(ret > 0)) { execute_data = EG(current_execute_data); ZEND_VM_LOOP_INTERRUPT_CHECK(); } else { return; } } } zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen"); }
这里,我去掉了一些环境变量判断分支,保留了运行的主流程。可以看到,在一个无限循环中,虚拟机会不断调用 OPCODE 指定的 handler
函数处理指令集,直到某次指令处理的结果 ret
小于0。注意到,在主流程中并没有移动 OPCODE 数组的当前指针,而是把这个过程放到指令执行的具体函数的结尾。所以,我们在大多数 OPCODE 的实现函数的末尾,都能看到调用这个宏:
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
在之前那个简单例子中,我们看到 vld 打印出的执行 OPCODE 数组中,最后有一项指令为 ZEND_RETURN
的 OPCODE。但我们编写的 PHP 代码中并没有这样的语句。在编译时期,虚拟机会自动将这个指令加到 OPCODE 数组的结尾。ZEND_RETURN
指令对应的函数会返回 -1,判断执行的结果小于0时,就会退出循环,从而结束程序的运行。
方法调用
如果我们调用一个自定义的函数,虚拟机会如何处理呢?
<?php function foo() { echo 'test'; } foo();
我们通过 vld 查看生成的 OPCODE。出现了两个 OPCODE 指令执行栈,是因为我们自定义了一个 PHP 函数。在第一个执行栈上,调用自定义函数会执行两个 OPCODE 指令:INIT_FC<a href="http://www.php.cn/wiki/1483.html" target="_blank">ALL</a>
和 DO_FCALL
。
compiled vars: none line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > NOP 6 1 INIT_FCALL 'foo' 2 DO_FCALL 0 3 > RETURN 1 compiled vars: none line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 3 0 E > ECHO 'test' 4 1 > RETURN null
其中,INIT_FCALL
准备了执行函数时所需要的上下文数据。DO_FCALL
负责执行函数。DO_FCALL
的处理函数根据不同的调用情况处理了大量逻辑,我摘取了其中执行用户定义的函数的逻辑部分:
ZEND_VM_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL)) { USE_OPLINE zend_execute_data *call = EX(call); zend_function *fbc = call->func; zend_object *object; zval *ret; ... if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) { ret = NULL; if (RETURN_VALUE_USED(opline)) { ret = EX_VAR(opline->result.var); ZVAL_NULL(ret); } call->prev_execute_data = execute_data; i_init_func_execute_data(call, &fbc->op_array, ret); if (EXPECTED(zend_execute_ex == execute_ex)) { ZEND_VM_ENTER(); } else { ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP); zend_execute_ex(call); } } ... ZEND_VM_SET_OPCODE(opline + 1); ZEND_VM_CONTINUE(); }
可以看到,DO_FCALL
首先将调用函数前的上下文数据保存到 call->prev_execute_data
,然后调用 i_init_func_execute_data
函数,将自定义函数对象中的 op_array
(每个自定义函数会在编译的时候生成对应的数据,其数据结构中包含了函数的 OPCODE 数组) 赋值给新的执行上下文对象。
然后,调用 zend_execute_ex
函数,开始执行自定义的函数。zend_execute_ex
实际上就是前面提到的 execute_ex
函数(默认是这样,但扩展可能重写 zend_execute_ex
指针,这个 API 让 PHP 扩展开发者可以通过覆写函数达到扩展功能的目的,不是本篇的主题,不准备深入探讨),只是上下文数据被替换成当前函数所在的上下文数据。
我们可以这样理解,最外层的代码就是一个默认存在的函数(类似 C 语言中的 main()
函数),和用户自定义的函数本质上是没有区别的。
逻辑跳转
我们知道指令都是顺序执行的,而我们的程序,一般都包含不少的逻辑判断和循环,这部分又是如何通过 OPCODE 实现的呢?
<?php $a = 10; if ($a == 10) { echo 'success'; } else { echo 'failure'; }
我们还是通过 vld 查看 OPCODE(不得不说 vld 扩展是分析 PHP 的神器)。
compiled vars: !0 = $a line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0, 10 3 1 IS_EQUAL ~2 !0, 10 2 > JMPZ ~2, ->5 4 3 > ECHO 'success' 4 > JMP ->6 6 5 > ECHO 'failure' 7 6 > > RETURN 1
我们看到,JMPZ
和 JMP
控制了执行流程。JMP
的逻辑非常简单,将当前的 OPCODE 指针指向需要跳转的 OPCODE。
ZEND_VM_HANDLER(42, ZEND_JMP, JMP_ADDR, ANY) { USE_OPLINE ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1)); ZEND_VM_CONTINUE(); }
JMPZ
仅仅是多了一次判断,根据结果选择是否跳转,这里就不再重复列举了。而处理循环的方式与判断基本上是类似的。
<?php $a = [1, 2, 3]; foreach ($a as $n) { echo $n; }
compiled vars: !0 = $a, !1 = $n line #* E I O op fetch ext return operands ------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 3 1 > FE_RESET_R $3 !0, ->5 2 > > FE_FETCH_R $3, !1, ->5 4 3 > ECHO !1 4 > JMP ->2 5 > FE_FREE $3 5 6 > RETURN 1
循环只需要 JMP
指令即可完成,通过 FE_FETCH_R
指令判断是否已经到达数组的结尾,如果到达则退出循环。
结语
通过了解 Zend 虚拟机,相信你对 PHP 是如何运行的,会有更深刻的理解。想到我们写的一行行代码,最后机器执行的时候会变成数不胜数的指令,每个指令又建立在复杂的处理逻辑之上。那些从前随意写下的代码,现在会不会在脑海里不自觉的转换成 OPCODE 再品味一番呢?
以上是PHP核心分析-Zend虛擬機器詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

本教程演示瞭如何使用PHP有效地處理XML文檔。 XML(可擴展的標記語言)是一種用於人類可讀性和機器解析的多功能文本標記語言。它通常用於數據存儲

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

靜態綁定(static::)在PHP中實現晚期靜態綁定(LSB),允許在靜態上下文中引用調用類而非定義類。 1)解析過程在運行時進行,2)在繼承關係中向上查找調用類,3)可能帶來性能開銷。

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元

PHP的魔法方法有哪些? PHP的魔法方法包括:1.\_\_construct,用於初始化對象;2.\_\_destruct,用於清理資源;3.\_\_call,處理不存在的方法調用;4.\_\_get,實現動態屬性訪問;5.\_\_set,實現動態屬性設置。這些方法在特定情況下自動調用,提升代碼的靈活性和效率。

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。
