這篇文章給大家分享的是有關(guān)PHP內(nèi)核中OPCODE是什么的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
創(chuàng)新互聯(lián)堅持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:網(wǎng)站建設(shè)、成都做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的蓬江網(wǎng)站設(shè)計、移動媒體設(shè)計的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
什么是 OPCODE?它是一種虛擬機能夠識別并處理的指令。Zend 虛擬機包含了一系列的 OPCODE,通過 OPCODE 虛擬機能夠做很多事情,列舉幾個 OPCODE 的例子:
ZEND_ADD
將兩個操作數(shù)相加。
ZEND_NEW
創(chuàng)建一個 PHP 對象。
ZEND_ECHO
將內(nèi)容輸出到標準輸出中。
ZEND_EXIT
退出 PHP。
諸如此類的操作,PHP 定義了186個(隨著 PHP 的更新,肯定會支持更多種類的 OPCODE),所有的 OPCODE 的定義和實現(xiàn)都可以在源碼的 zend/zend_vm_def.h
文件(這個文件的內(nèi)容并不是原生的 C 代碼,而是一個模板,后面會說明原因)中查閱到。
我們來看下 PHP 是如何設(shè)計 OPCODE 數(shù)據(jù)結(jié)構(gòu):
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 的數(shù)據(jù)結(jié)構(gòu),是不是能找到匯編語言的感覺。每一個 OPCODE 都包含兩個操作數(shù),op1
和 op2
,handler
指針則指向了執(zhí)行該 OPCODE 操作的函數(shù),函數(shù)處理后的結(jié)果,會被保存在 result
中。
我們舉一個簡單的例子:
我們通過 vld 擴展看到,經(jīng)過編譯的后,上面的代碼生成了 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個操作數(shù),op1
是變量$b
,op2
是數(shù)字常量1,返回的結(jié)果存入了臨時變量中。在zend/zend_vm_def.h
文件中,我們可以找到 ZEND_ADD 指令對應(yīng)的函數(shù)實現(xiàn):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 = GE***_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 是弱類型語言,而其實現(xiàn)的 C 則是強類型語言。弱類型語言支持自動類型匹配,而自動類型匹配的實現(xiàn)方式,就像上述代碼一樣,通過判斷來處理不同類型的參數(shù)。試想一下,如果每一個 OPCODE 處理的時候都需要判斷傳入的參數(shù)類型,那么性能勢必成為極大的問題(一次請求需要處理的 OPCODE 可能能達到成千上萬個)。
哪有什么辦法嗎?我們發(fā)現(xiàn)在編譯的時候,已經(jīng)能夠確定每個操作數(shù)的類型(可能是常量還是變量)。所以,PHP 真正執(zhí)行時的 C 代碼,不同類型操作數(shù)將分成不同的函數(shù),供虛擬機直接調(diào)用。這部分代碼放在了
zend/zend_vm_execute.h
中,展開后的文件相當大,而且我們注意到還有這樣的代碼:if (IS_CONST == IS_CV) {完全沒有什么意義是吧?不過沒有關(guān)系,C 的編譯器會自動優(yōu)化這樣判斷。大多數(shù)情況,我們希望了解某個 OPCODE 處理的邏輯,還是通過閱讀模板文件
zend/zend_vm_def.h
比較容易。順便說一下,根據(jù)模板生成 C 代碼的程序就是用 PHP 實現(xiàn)的。執(zhí)行過程
準確的來說,PHP 的執(zhí)行分成了兩大部分:編譯和執(zhí)行。這里我將不會詳細展開編譯的部分,而是把焦點放在執(zhí)行的過程。
通過語法、詞法分析等一系列的編譯過程后,我們得到了一個名為 OPArray 的數(shù)據(jù),其結(jié)構(gòu)如下:
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]; };內(nèi)容超多對吧?簡單的理解,其本質(zhì)就是一個 OPCODE 數(shù)組外加執(zhí)行過程中所需要的環(huán)境數(shù)據(jù)的集合。介紹幾個相對來說比較重要的字段:
opcodes
存放 OPCODE 的數(shù)組。
filename
當前執(zhí)行的腳本的文件名。
function_name
當前執(zhí)行的方法名稱。
static_variables
靜態(tài)變量列表。
last_try_catch
try_catch_array
當前上下文中,如果出現(xiàn)異常 try-catch-finally 跳轉(zhuǎn)所需的信息。
literals
所有諸如字符串 foo 或者數(shù)字23,這樣的常量字面量集合。
為什么需要生成這樣龐大的數(shù)據(jù)?因為編譯時期生成的信息越多,執(zhí)行時期所需要的時間就越少。
接下來,我們看下 PHP 是如何執(zhí)行 OPCODE。OPCODE 的執(zhí)行被放在一個大循環(huán)中,這個循環(huán)位于 zend/zend_vm_execute.h
中的 execute_ex
函數(shù):
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"); }
這里,我去掉了一些環(huán)境變量判斷分支,保留了運行的主流程??梢钥吹?,在一個***循環(huán)中,虛擬機會不斷調(diào)用 OPCODE 指定的 handler
函數(shù)處理指令集,直到某次指令處理的結(jié)果 ret
小于0。注意到,在主流程中并沒有移動 OPCODE 數(shù)組的當前指針,而是把這個過程放到指令執(zhí)行的具體函數(shù)的結(jié)尾。所以,我們在大多數(shù) OPCODE 的實現(xiàn)函數(shù)的末尾,都能看到調(diào)用這個宏:
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
在之前那個簡單例子中,我們看到 vld 打印出的執(zhí)行 OPCODE 數(shù)組中,***有一項指令為 ZEND_RETURN
的 OPCODE。但我們編寫的 PHP 代碼中并沒有這樣的語句。在編譯時期,虛擬機會自動將這個指令加到 OPCODE 數(shù)組的結(jié)尾。ZEND_RETURN
指令對應(yīng)的函數(shù)會返回 -1,判斷執(zhí)行的結(jié)果小于0時,就會退出循環(huán),從而結(jié)束程序的運行。
如果我們調(diào)用一個自定義的函數(shù),虛擬機會如何處理呢?
我們通過 vld 查看生成的 OPCODE。出現(xiàn)了兩個 OPCODE 指令執(zhí)行棧,是因為我們自定義了一個 PHP 函數(shù)。在***個執(zhí)行棧上,調(diào)用自定義函數(shù)會執(zhí)行兩個 OPCODE 指令:
INIT_FCALL
和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 1compiled vars: none line #* E I O op fetch ext return operands------------------------------------------------------------------------------------- 3 0 E > ECHO 'test' 4 1 > RETURN null其中,
INIT_FCALL
準備了執(zhí)行函數(shù)時所需要的上下文數(shù)據(jù)。DO_FCALL
負責(zé)執(zhí)行函數(shù)。DO_FCALL
的處理函數(shù)根據(jù)不同的調(diào)用情況處理了大量邏輯,我摘取了其中執(zhí)行用戶定義的函數(shù)的邏輯部分: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
首先將調(diào)用函數(shù)前的上下文數(shù)據(jù)保存到call->prev_execute_data
,然后調(diào)用i_init_func_execute_data
函數(shù),將自定義函數(shù)對象中的op_array
(每個自定義函數(shù)會在編譯的時候生成對應(yīng)的數(shù)據(jù),其數(shù)據(jù)結(jié)構(gòu)中包含了函數(shù)的 OPCODE 數(shù)組) 賦值給新的執(zhí)行上下文對象。然后,調(diào)用
zend_execute_ex
函數(shù),開始執(zhí)行自定義的函數(shù)。zend_execute_ex
實際上就是前面提到的execute_ex
函數(shù)(默認是這樣,但擴展可能重寫zend_execute_ex
指針,這個 API 讓 PHP 擴展開發(fā)者可以通過覆寫函數(shù)達到擴展功能的目的,不是本篇的主題,不準備深入探討),只是上下文數(shù)據(jù)被替換成當前函數(shù)所在的上下文數(shù)據(jù)。我們可以這樣理解,最外層的代碼就是一個默認存在的函數(shù)(類似 C 語言中的
main()
函數(shù)),和用戶自定義的函數(shù)本質(zhì)上是沒有區(qū)別的。邏輯跳轉(zhuǎn)
我們知道指令都是順序執(zhí)行的,而我們的程序,一般都包含不少的邏輯判斷和循環(huán),這部分又是如何通過 OPCODE 實現(xiàn)的呢?
我們還是通過 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
控制了執(zhí)行流程。JMP
的邏輯非常簡單,將當前的 OPCODE 指針指向需要跳轉(zhuǎn)的 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
僅僅是多了一次判斷,根據(jù)結(jié)果選擇是否跳轉(zhuǎn),這里就不再重復(fù)列舉了。而處理循環(huán)的方式與判斷基本上是類似的。compiled vars: !0 = $a, !1 = $nline #* E I O op fetch ext return operands------------------------------------------------------------------------------------- 2 0 E > ASSIGN !0,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 循環(huán)只需要
JMP
指令即可完成,通過FE_FETCH_R
指令判斷是否已經(jīng)到達數(shù)組的結(jié)尾,如果到達則退出循環(huán)。感謝各位的閱讀!關(guān)于“PHP內(nèi)核中OPCODE是什么”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
分享標題:PHP內(nèi)核中OPCODE是什么
文章地址:http://weahome.cn/article/gheheg.html