PHP核心探索:zend_execute的具體執行過程
解譯器引擎最終執行op的函數是zend_execute,實際上zend_execute是一個函數指標,在引擎初始化的時候zend_execute預設指向了execute,這個execute定義在{PHPSRC}/Zend/zend_vm_execute.h:
ZEND_API void execute(zend_op_array *op_array TSRMLS_DC) { zend_execute_data *execute_data; zend_bool nested = 0; zend_bool original_in_execution = EG(in_execution); if (EG(exception)) { return; } EG(in_execution) = 1; zend_vm_enter: /* Initialize execute_data */ execute_data = (zend_execute_data *)zend_vm_stack_alloc( ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2)) + ZEND_MM_ALIGNED_SIZE(sizeof(temp_variable)) * op_array->T TSRMLS_CC); EX(CVs) = (zval***)((char*)execute_data + ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data))); memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var); EX(Ts) = (temp_variable *)(((char*)EX(CVs)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2))); EX(fbc) = NULL; EX(called_scope) = NULL; EX(object) = NULL; EX(old_error_reporting) = NULL; EX(op_array) = op_array; EX(symbol_table) = EG(active_symbol_table); EX(prev_execute_data) = EG(current_execute_data); EG(current_execute_data) = execute_data; EX(nested) = nested; nested = 1; if (op_array->start_op) { ZEND_VM_SET_OPCODE(op_array->start_op); } else { ZEND_VM_SET_OPCODE(op_array->opcodes); } if (op_array->this_var != -1 && EG(This)) { Z_ADDREF_P(EG(This)); /* For $this pointer */ if (!EG(active_symbol_table)) { EX(CVs)[op_array->this_var] = (zval**)EX(CVs) + (op_array->last_var + op_array->this_var); *EX(CVs)[op_array->this_var] = EG(This); } else { if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void**)&EX(CVs)[op_array->this_var])==FAILURE) { Z_DELREF_P(EG(This)); } } } EG(opline_ptr) = &EX(opline); EX(function_state).function = (zend_function *) op_array; EX(function_state).arguments = NULL; while (1) { int ret; #ifdef ZEND_WIN32 if (EG(timed_out)) { zend_timeout(0); } #endif if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) { switch (ret) { case 1: EG(in_execution) = original_in_execution; return; case 2: op_array = EG(active_op_array); goto zend_vm_enter; case 3: execute_data = EG(current_execute_data); default: break; } } } zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen"); }
此函數的參數為op_array,這是一個指向zend_op_array的指標,op_array是在編譯過程中產生,這裡有必要介紹一下zend_op_array這個類型。
zend_op_array簡介
此類型的定義在{PHPSRC}/Zend/zend_compile.h:
struct _zend_op_array { /* Common elements */ zend_uchar type; char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsigned char return_reference; /* END of common elements */ zend_bool done_pass_two; zend_uint *refcount; zend_op *opcodes; zend_uint last, size; zend_compiled_variable *vars; int last_var, size_var; zend_uint T; zend_brk_cont_element *brk_cont_array; int last_brk_cont; int current_brk_cont; zend_try_catch_element *try_catch_array; int last_try_catch; /* static variables support */ HashTable *static_variables; zend_op *start_op; int backpatch_count; zend_uint this_var; char *filename; zend_uint line_start; zend_uint line_end; char *doc_comment; zend_uint doc_comment_len; zend_uint early_binding; /* the linked list of delayed declarations */ void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; typedef struct _zend_op_array zend_op_array;
此結構比較複雜,我們目前只介紹最基本的幾個欄位。
1.type:
op_array的類型,首先需要說明的是,一段PHP代碼被編譯之後,雖然返回的是一個zend_op_array指標,但是實際上產生的zend_op_array結構可能不止一個,通過這個結構中的一些欄位,例如function_name ,num_args等你也許會發現這個zend_op_array結構似乎能和函數產生一定的聯絡,確實如此,使用者自訂的函數,以及使用者定義的類的方法,都是一個zend_op_array結構,這些zend_op_array結構在編譯過程中被儲存在某些地方,例如使用者自訂的函數被儲存進了GLOBAL_FUNCTION_TABLE,這個是全域函數符號表,通過函數名可以在此表中檢索到函數體。那麼編譯後返回的那個zend_op_array指標是什麼呢,其實編譯後返回的zend_op_array是執行的一個入口,也可以認為它是最外層,即不在任何函數體內的全域程式碼群組成的op_array。然而全域代碼,使用者自訂函數,使用者自訂的方法都擁有相同的type值:2 ,type可取值的宏定義為:
#define ZEND_INTERNAL_FUNCTION 1 #define ZEND_USER_FUNCTION 2 #define ZEND_OVERLOADED_FUNCTION 3 #define ZEND_EVAL_CODE 4 #define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5
可以看到全域代碼,使用者函數,使用者方法都對應的是ZEND_USER_FUNCTION,這個也是最常見的type了,其中ZEND_EVAL_CODE對應的是eval函數中的PHP代碼,所以我們可以想到,eval函數參數中的PHP代碼也會被編譯成單獨的zend_op_array。
2.function_name
如果op_array是由使用者定義的函數或則方法編譯而產生,那麼此欄位對應函數的名字,如果是全域代碼或則是eval部分的代碼,那麼此欄位為控制。
3.opcodes
這個欄位類型為zend_op *,因此這是一個zend_op的數組,這個數組儲存的就是此編譯過程中產生的op,如果不瞭解zend_op,可以看看之前的文章 OPcode簡介 , 這個欄位是最重要的部分了,zend_execute最終就是執行這裡儲存的op。
現在基本對參數op_array有了一定的瞭解,那麼我們就開始進入到execute中。
執行過程詳解
execute函數開始的時候是一些基礎變數的申明,其中zend_execute_data *execute_data;是執行期的資料結構,此變數在進行一定的初始化之後將會被傳遞給每個op的handler函數作為參數,op在執行過程中隨時有可能改變execute_data中的內容。
第14行zend_vm_enter 這個跳轉標籤是作為虛擬機器執行的入口,當op中涉及到函數調用的時候,就有可能會跳轉到這裡來執行函數體。
第16行到第19行為execute_data分配空間
第21行到第32行主要是對execute_data進行一些初始化,以及儲存現場工作,要儲存現場是因為在進入函數調用的時候,需要儲存當前一些運行期間的資料,在函數調用結束之後再進行還原,可以想象為作業系統中進程調度,當進程在調出的時候需要儲存寄存器等上下文環境,而當進程被調入的時候再取出來繼續執行。
第41行到第51行主要是在當前動態符號表中加入$this變數,這個是在調用對象的方法時才有必要進行。
第58行開始的while無限迴圈就是開始執行op_array中的opcodes了,在第66行中調用當前執行的op的handler:
EX(opline)->handler(execute_data TSRMLS_CC))
然後如果handler的傳回值小於0則迴圈繼續,如果大於0則進入一個switch結構:
當傳回值為1時:execute函數將返回,執行也就結束了。
當傳回值為2時:op_array被重新設定,並跳轉到zend_vm_enter ,這個一般是函數調用或則執行eval函數中的代碼,將在新的上下文執行相關函數的op_array
當傳回值為3時:迴圈體繼續繼續執行,當然再繼續執行之前,EX(opline)已經往後移了一位(可能多位),也就是已經指向了後面一個新的opline,於是繼續執行新的opline
當返回其他值時:結束迴圈,報錯,結束應該用return,也就是返回1
在op的handler中返回特定的值都被定義成了宏,例如{PHPSRC}/Zend/zend_execute.c中定義的:
#define ZEND_VM_NEXT_OPCODE() / CHECK_SYMBOL_TABLES() / EX(opline)++; / ZEND_VM_CONTINUE() #define ZEND_VM_SET_OPCODE(new_op) / CHECK_SYMBOL_TABLES() / EX(opline) = new_op #define ZEND_VM_JMP(new_op) / CHECK_SYMBOL_TABLES() / if (EXPECTED(!EG(exception))) { / EX(opline) = new_op; / } / ZEND_VM_CONTINUE() #define ZEND_VM_INC_OPCODE() / EX(opline)++
以及在{PHPSRC}/Zend/zend_vm_execute.c中定義的:
#define ZEND_VM_CONTINUE() return 0 #define ZEND_VM_RETURN() return 1 #define ZEND_VM_ENTER() return 2 #define ZEND_VM_LEAVE() return 3 #define ZEND_VM_DISPATCH(opcode, opline) return zend_vm_get_opcode_handler(opcode, opline)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
簡單介紹功能
ZEND_VM_NEXT_OPCODE():移動到下一條op,返回0,不進入switch,迴圈繼續(這個是最常用到的)
ZEND_VM_SET_OPCODE(new_op):當前opline設定成new_op
ZEND_VM_JMP(new_op) :當前opline設定成new_op,返回0,不進入switch,迴圈繼續
ZEND_VM_INC_OPCODE():僅僅移動到下一條op
執行環境的切換
在前面的內容已經提到,使用者自訂函數,類方法,eval的代碼都會編譯成單獨的op_array,那麼當進行函數調用等操作時,必然涉及到調用前的op_array執行環境和新的函數的op_array執行環境的切換,這一段我們將以調用使用者自訂函數來介紹整個切換過程如何進行。
介紹此過程前必須瞭解執行環境的相關資料結構,涉及到執行環境的資料結構主要有兩個:
1. 執行期全域變數結構
相關的定義在{PHPSRC}/Zend/zend_globals_macros.h:
/* Executor */ #ifdef ZTS # define EG(v) TSRMG(executor_globals_id, zend_executor_globals *, v) #else # define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals; #endif
這裡是一個條件編譯,ZTS表示安全執行緒啟用,為了簡化,我們這裡以非安全執行緒模式的情況下來介紹,那麼執行期的全域變數就是executor_globals,其類型為zend_executor_globals, zend_executor_globals的定義在{PHPSRC}/Zend/zend_globals.h,結構比較龐大,這裡包含了整個執行期需要用到的各種變數,無論是哪個op_array在執行,都共用這一個全域變數,在執行過程中,此結構中的一些成員可能會改變,比如當前執行的op_array欄位active_op_array,動態符號表欄位active_symbol_table可能會根據不同的op_array而改變,This指標會根據在不同的對象環境而改變。
另外還定義了一個EG宏來取此變數中的欄位值,此宏是針對安全執行緒和非安全執行緒模式的一個封裝。
2.每個op_array自身的執行資料
針對每一個op_array,都會有自己執行期的一些資料,在函數execute開始的時候我們能看到zend_vm_enter跳轉標籤下面就會初始一個局部變數execute_data,所以我們每次切換到新的op_array的時候,都會為新的op_array建立一個execute_data變數,此變數的類型為zend_execute_data的指標,相關定義在{PHPSRC}/Zend/zend_compile.h:
struct _zend_execute_data { struct _zend_op *opline; zend_function_state function_state; zend_function *fbc; /* Function Being Called */ zend_class_entry *called_scope; zend_op_array *op_array; zval *object; union _temp_variable *Ts; zval ***CVs; HashTable *symbol_table; struct _zend_execute_data *prev_execute_data; zval *old_error_reporting; zend_bool nested; zval **original_return_value; zend_class_entry *current_scope; zend_class_entry *current_called_scope; zval *current_this; zval *current_object; struct _zend_op *call_opline; };
可以用EX宏來取其中的值:#define EX(element) execute_data->element
這裡只簡單介紹其中兩個欄位:
opline: 當前正在執行的op。
prev_execute_data: op_array環境切換的時候,這個欄位用來儲存切換前的op_array,此欄位非常重要,他能將每個op_array的execute_data按照調用的先後順序串連成一個單鏈表,每當一個op_array執行結束要還原到調用前op_array的時候,就通過當前的execute_data中的prev_execute_data欄位來得到調用前的執行器資料。
在executor_globals中的欄位current_execute_data就是指向當前正在執行的op_array的execute_data。
再正式介紹之前還需要簡單的介紹一下使用者自訂函數的調用過程,詳細的過程以後再函數章節中專門介紹,這裡簡單的說明一下:
在調用函數的時候,比如test()函數,會先在全域函數符號表中根據test來搜尋相關的函數體,如果搜尋不到則會報錯函數沒有定義,找到test的函數體之後,取得test函數的op_array,然後跳轉到execute中的goto標籤:zend_vm_enter,於是就進入到了test函數的執行環境。
下面我們將以一段簡單的代碼來介紹執行環境切換過程,例子代碼:
這段代碼非常簡單,這樣方便我們介紹原理,複雜的代碼讀者可以舉一反三。此代碼編譯之後會產生兩個op_array,一個是全域代碼的op_array,另外一個是test函數的op_array,其中全域代碼中會通過函數調用進入到test函數的執行環境,執行結束之後,會返回到全域代碼,然後代碼結束。
下面我們分幾個階段來介紹這段代碼的過程,然後從中可以知道執行環境切換的方法。
1. 進入execute函數,開始執行op_array ,這個op_array就是全域代碼的op_array,我們暫時稱其為op_array1
首先在execute中為op_array1建立了一個execute_data資料,我們暫時命名為execute_data1,然後進行相關的初始化操作,其中比較重要的是:
EX(op_array) = op_array; // 設定op_array欄位為當前執行的op_array,也就是全域代碼的op_array1EX(prev_execute_data) = EG(current_execute_data);//將全域執行資料中儲存的當前op_array執行資料儲存到op_array1的execute_data1的prev_execute_data欄位,由於這是執行的第一個op_array,所以prev_execute_data實際上是空值,然後將執行期全域變數的current_execute_data設定成execute_data1,然後設定execute_data1的當前執行op,這樣就可以開始執行當前的op了
2. 在op_array1執行到test函數調用的的時候,首先從全域函數符號表中找到test的函數體,將函數體儲存在execute_data1的function_state欄位,然後從函數體中取到test的op_array,我們這裡用op_array2來表示,並將op_array2賦值給EG(active_op_array):
EG(active_op_array) = &EX(function_state).function->op_array;
於是執行期全域變數的動態op_array欄位指向了函數test的op_array,然後用調用ZEND_VM_ENTER();這個時候會先回到execute函數中的switch結構,並且滿足以下case
case 2: op_array = EG(active_op_array); goto zend_vm_enter;
EG(active_op_array)之前已經被我們設定為test函數的op_array2,於是在函數execute中,op_array變數就指向了test的op_array2,然後跳轉到zend_vm_enter。
3. 跳轉到zend_vm_enter之後其實又回到了類似1中的步驟,此時為test的op_array2建立了它的執行資料execute_data,我們這裡用execute_data2來表示。跟1中有些不同的是EX(prev_execute_data) = EG(current_execute_data);這個時候current_execute_data = execute_data1,也就是全域代碼的執行執行期資料,然後EG(current_execute_data) = execute_data;這樣current_execute_data就等於test的執行期資料execute_data2了,同時全域代碼的execute_data1被儲存在execute_data2的prev_execute_data欄位。這個時候進行環境的切換已經完成,於是開始執行test函數。
4. test函數執行完之後就要返回到調用前的執行環境了,也就是全域代碼執行環境,此階段最重要的一個操作就是EG(current_execute_data) = EX(prev_execute_data); 在3中EX(prev_execute_data)已經設定成了全域代碼的execute_data1,所以這樣當前執行資料就變成了全域代碼的執行資料,這樣就成功的從函數test執行環境返回到了全域代碼執行環境
這樣,執行環境的切換過程就完成了,對於深層次的函數調用,原理一樣,執行資料execute_data組成的單鏈表會更長。
延伸閱讀
此文章所在專題列表如下:
PHP核心探索:從SAPI介面開始
PHP核心探索:一次請求的開始與結束
PHP核心探索:一次請求生命週期
PHP核心探索:單進程SAPI生命週期
PHP核心探索:多進程/線程的SAPI生命週期
PHP核心探索:Zend引擎
PHP核心探索:再次探討SAPI
PHP核心探索:Apache模組介紹
PHP核心探索:通過mod_php5支援PHP
PHP核心探索:Apache運行與鉤子函數
PHP核心探索:嵌入式PHP
PHP核心探索:PHP的FastCGI
PHP核心探索:如何執行PHP指令碼
PHP核心探索:PHP指令碼的執行細節
PHP核心探索:作業碼OpCode
PHP核心探索:PHP裡的opcode
PHP核心探索:解譯器的執行過程
PHP核心探索:變數概述
PHP核心探索:變數儲存與類型
PHP核心探索:PHP中的雜湊表
PHP核心探索:理解Zend裡的雜湊表
PHP核心探索:PHP雜湊演算法設計
PHP核心探索:翻譯一篇HashTables文章
PHP核心探索:雜湊碰撞攻擊是什嗎?
PHP核心探索:常量的實現
PHP核心探索:變數的儲存
PHP核心探索:變數的類型
PHP核心探索:變數的值操作
PHP核心探索:變數的建立
PHP核心探索:預定義變數
PHP核心探索:變數的檢索
PHP核心探索:變數的類型轉換
PHP核心探索:弱類型變數的實現
PHP核心探索:靜態變數的實現
PHP核心探索:變數類型提示
PHP核心探索:變數的生命週期
PHP核心探索:變數賦值與銷毀
PHP核心探索:變數範圍
PHP核心探索:詭異的變數名
PHP核心探索:變數的value和type儲存
PHP核心探索:全域變數Global
PHP核心探索:變數類型的轉換
PHP核心探索:記憶體管理開篇
PHP核心探索:Zend記憶體管理器
PHP核心探索:PHP的記憶體管理
PHP核心探索:記憶體的申請與銷毀
PHP核心探索:引用計數與寫時複製
PHP核心探索:PHP5.3的記憶體回收機制
PHP核心探索:記憶體管理中的cache
PHP核心探索:寫時複製COW機制
PHP核心探索:數組與鏈表
PHP核心探索:使用雜湊表API
PHP核心探索:數組操作
PHP核心探索:數組源碼分析
PHP核心探索:函數的分類
PHP核心探索:函數的內部結構
PHP核心探索:函數結構轉換
PHP核心探索:定義函數的過程
PHP核心探索:函數的參數
PHP核心探索:zend_parse_parameters函數
PHP核心探索:函數傳回值
PHP核心探索:形參return value
PHP核心探索:函數調用與執行
PHP核心探索:引用與函數執行
PHP核心探索:匿名函數及閉包
PHP核心探索:物件導向開篇
PHP核心探索:類的結構和實現
PHP核心探索:類的成員變數
PHP核心探索:類的成員方法
PHP核心探索:類的原型zend_class_entry
PHP核心探索:類的定義
PHP核心探索:存取控制
PHP核心探索:繼承,多態與抽象類別
PHP核心探索:魔術函數與延遲綁定
PHP核心探索:保留類與特殊類
PHP核心探索:對象
PHP核心探索:建立對象執行個體
PHP核心探索:對象屬性讀寫
PHP核心探索:命名空間
PHP核心探索:定義介面
PHP核心探索:繼承與實現介面
PHP核心探索:資源resource類型
PHP核心探索:Zend虛擬機器
PHP核心探索:虛擬機器的詞法解析
PHP核心探索:虛擬機器的文法分析
PHP核心探索:中間代碼opcode的執行
PHP核心探索:代碼的加密與解密
PHP核心探索:zend_execute的具體執行過程
PHP核心探索:變數的引用與計數規則
PHP核心探索:新記憶體回收機制說明
參考來源:
PHP核心探索:zend_execute的具體執行過程
http://www.lai18.com/content/425167.html