PHP內核解密系列:zend_execute的執行過程
解釋器引擎最終執行op的函數是zend_execute,實際上zend_execute是一個函數指針,在引擎初始化的時候zend_execute,實際上zend_execute是一個函數指針,在引擎初始化的時候zend_execute( PHPSRC}/Zend/zend_vm_execute.h: zend_op_array簡介 執行過程詳解 执行环境的切换 延伸閱讀
此函數的參數為op_array,這是一個指向zend_op_array的指針,op_array是在編譯過程中生成,這裡有必要介紹一下zend_op_array這個類型。
參考來源:http://www.lai18.com/content/425167.html
此結構比較複雜,我們目前只如此介紹最基本的幾個欄位。
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可取值的巨集定義為:
可以看到全域程式碼,使用者函數,使用者方法都對應的是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。
第14行zend_vm_enter 这个跳转标签是作为虚拟机执行的入口,当op中涉及到函数调用的时候,就有可能会跳转到这里来执行函数体。
第16行到第19行为execute_data分配空间
第21行到第32行主要是对execute_data进行一些初始化,以及保存现场工作,要保存现场是因为在进入函数调用的时候,需要保存当前一些运行期间的数据,在函数调用结束之后再进行还原,可以想象为操作系统中进程调度,当进程在调出的时候需要保存寄存器等上下文环境,而当进程被调入的时候再取出来继续执行。
第41行到第51行主要是在当前动态符号表中加入$this变量,这个是在调用对象的方法时才有必要进行。
第58行开始的while无限循环就是开始执行op_array中的opcodes了,在第66行中调用当前执行的op的handler:
然后如果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中定义的:
以及在{PHPSRC}/Zend/zend_vm_execute.c中定义的:
简单介绍功能
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
介绍此过程前必须了解执行环境的相关数据结构,涉及到执行环境的数据结构主要有两个:
1. 执行期全局变量结构
相关的定义在{PHPSRC}/Zend/zend_globals_macros.h:
这里是一个条件编译,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:
可以用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,然后进行相关的初始化操作,其中比较重要的是:
2. 在op_array1执行到test函数调用的的时候,首先从全局函数符号表中找到test的函数体,将函数体保存在execute_data1的function_state字段,然后从函数体中取到test的op_array,我们这里用op_array2来表示,并将op_array2赋值给EG(active_op_array):
于是执行期全局变量的动态op_array字段指向了函数test的op_array,然后用调用ZEND_VM_ENTER();这个时候会先回到execute函数中的switch结构,并且满足以下case
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核心探索:Apacheache模組介紹
PHP核心探索:透過mod_php5支援PHP
PHPPache模組介紹
PHP核心探索:透過mod_php5支援PHP
PHPPache模組介紹
PHP核心探索:透過mod_php5支援PHP
PHPPache模組介紹
PHP核心探索:透過mod_php5支援PHP
PHPPache模組介紹
PHP核心探索:透過mod_php5支援PHP
PHPP核心模組介紹: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核心探索:變數作用域
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核心解密系列:zend_execute的執行過程,包含了面向的內容,希望對PHP教學有興趣的朋友有幫助。