PHP系列之鉤子

PHP 提供的鉤子

PHP 和 Zend Engine 爲擴展提供了許多不一樣的鉤子,這些擴展容許擴展開發人員以 PHP userland 沒法提供的方式控制 PHP 運行時。算法

本章將展現各類鉤子和從擴展鉤子到它們的常見用例。編程

鉤子到 PHP 功能的通常模式是 PHP 核心提供的擴展覆蓋函數指針。而後擴展函數一般執行本身的工做並調用原始 PHP 核心函數。使用此模式,不一樣的擴展能夠覆蓋同一個鉤子而不會致使衝突。數組

掛鉤到函數的執行

userland和內部函數的執行由Zend引擎中的兩個函數處理,您能夠用本身的實現替換這兩個函數。覆蓋此鉤子的擴展的主要用例是通用函數級評測、調試和麪向方面的編程。緩存

鉤子在 Zend/zend_execute.h 中定義:安全

ZEND_API extern void (*zend_execute_ex)(zend_execute_data *execute_data);ZEND_API extern void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value);

若是要覆蓋這些函數指針,則必須在 Minit 中執行此操做,由於 Zend Engine 中的其餘決策是根據指針是否被覆蓋這一事實提早作出的。函數

覆蓋的一般模式是這樣的:性能

static void (*original_zend_execute_ex) (zend_execute_data *execute_data);static void (*original_zend_execute_internal) (zend_execute_data *execute_data, zval *return_value);void my_execute_internal(zend_execute_data *execute_data, zval *return_value);void my_execute_ex (zend_execute_data *execute_data);PHP_MINIT_FUNCTION(my_extension){

    REGISTER_INI_ENTRIES();

 

    original_zend_execute_internal = zend_execute_internal;

    zend_execute_internal = my_execute_internal;

 

    original_zend_execute_ex = zend_execute_ex;

    zend_execute_ex = my_execute_ex;

 

    return SUCCESS;}PHP_MSHUTDOWN_FUNCTION(my_extension){

    zend_execute_internal = original_zend_execute_internal;

    zend_execute_ex = original_zend_execute_ex;

 

    return SUCCESS;}

覆蓋 zend_execute_ex 的一個缺點是它將 Zend Virtual Machine 運行時的行爲更改成使用遞歸,而不是在不離開解釋器循環的狀況下處理調用。此外,沒有覆蓋zend_execute_ex的 PHP 引擎也能夠生成更優化的函數調用操做碼。優化

這些掛鉤對性能很是敏感,具體取決於原始函數封裝代碼的複雜性。ui

覆蓋內部功能

在覆蓋執行鉤子時,擴展能夠記錄每一個函數調用,你還能夠覆蓋用戶域,核心和擴展函數(和方法)的各個函數指針。若是擴展僅須要訪問特定的內部函數調用,則具備更好的性能特徵。加密

#if PHP_VERSION_ID < 70200typedef void (*zif_handler)(INTERNAL_FUNCTION_PARAMETERS);#endif

zif_handler original_handler_var_dump;ZEND_NAMED_FUNCTION(my_overwrite_var_dump){

    // 若是咱們想調用原始函數

    original_handler_var_dump(INTERNAL_FUNCTION_PARAM_PASSTHRU);}PHP_MINIT_FUNCTION(my_extension){

    zend_function *original;

 

    original = zend_hash_str_find_ptr(EG(function_table), "var_dump", sizeof("var_dump")-1);

 

    if (original != NULL) {

        original_handler_var_dump = original->internal_function.handler;

        original->internal_function.handler = my_overwrite_var_dump;

    }}

 

覆蓋類方法時,能夠在 zend_class_entry上找到函數表:

zend_class_entry *ce = zend_hash_str_find_ptr(CG(class_table), "PDO", sizeof("PDO")-1);if (ce != NULL) {

    original = zend_hash_str_find_ptr(&ce->function_table, "exec", sizeof("exec")-1);

 

    if (original != NULL) {

        original_handler_pdo_exec = original->internal_function.handler;

        original->internal_function.handler = my_overwrite_pdo_exec;

    }}

 

修改抽象語法樹(AST)

當 PHP 7編譯 PHP 代碼時,它會先將其轉換爲抽象語法樹(AST),而後最終生成持久存儲在 Opcache 中的操做碼。zend_ast_process鉤子會被每一個已編譯的腳本調用,並容許你在解析和建立 AST 以後修改 AST。

這是要使用的最複雜的鉤子之一,由於它須要徹底瞭解 AST。在此處建立無效的 AST 可能會致使異常行爲或崩潰。

最好看看使用此鉤子的示例擴展:

  • Google Stackdriver PHP調試器擴展
  • 基於 Stackdriver 的帶有 AST 的概念驗證器

熟悉腳本/文件編譯

每當用戶腳本調用include/require或其對應的include_once/require_once時,PHP內核都會在指針zend_compile_file處調用該函數處理此請求。參數是文件句柄,結果是zend_op_array。

zend_op_array * my_extension_compile_file(zend_file_handle * file_handle,int類型);

PHP核心中有兩個擴展實現了此掛鉤:dtrace和opcache。

  • 若是您使用環境變量USE_ZEND_DTRACE啓動PHP腳本並使用dtrace支持編譯了PHP,則dtrace_compile_file用於Zend / zend_dtrace.c。
  • Opcache將操做數組存儲在共享內存中以得到更好的性能,所以,每當腳本被編譯時,其最終的操做數組都會從緩存中獲得服務,而不是從新編譯。您能夠在ext / opcache / ZendAccelerator.c中找到此實現。
  • 名爲compile_file的默認實現是Zend / zend_language_scanner.l中掃描程序代碼的一部分。

實施此掛鉤的用例是Opcode Accelerating,PHP代碼加密/解密,調試或概要分析。

您能夠隨時在執行PHP進程時替換該掛鉤,而且替換後編譯的全部PHP腳本都將由該掛鉤的實現處理。

始終調用原始函數指針很是重要,不然PHP將沒法再編譯腳本,而且Opcache將再也不起做用。

此處的擴展覆蓋順序也很重要,由於您須要知道是要在Opcache以前仍是以後註冊鉤子,由於Opcache若是在其共享內存緩存中找到操做碼數組條目,則不會調用原始函數指針。 Opcache將其鉤子註冊爲啓動後鉤子,該鉤子在擴展的minit階段以後運行,所以默認狀況下,緩存腳本時將再也不調用該鉤子。

調用錯誤處理程序時的通知

與PHP用戶區set_error_handler()函數相似,擴展能夠經過實現zend_error_cb鉤子將自身註冊爲錯誤處理程序:

ZEND_API void(* zend_error_cb)(int類型,const char * error_filename,const uint32_t error_lineno,const char * format,va_list args);

type變量對應於E _ *錯誤常量,該常量在PHP用戶區中也可用。

PHP核心和用戶態錯誤處理程序之間的關係很複雜:

  1. 若是未註冊任何用戶級錯誤處理程序,則始終調用zend_error_cb。
  2. 若是註冊了userland錯誤處理程序,則對於E_ERROR,E_PARSE,E_CORE_ERROR,E_CORE_WARNING,E_COMPILE_ERROR的全部錯誤和E_COMPILE_WARNING始終調用zend_error_cb掛鉤。
  3. 對於全部其餘錯誤,僅在用戶態處理程序失敗或返回false時調用zend_error_cb。

另外,因爲Xdebug自身複雜的實現,它以不調用之前註冊的內部處理程序的方式覆蓋錯誤處理程序。

所以,覆蓋此掛鉤不是很可靠。

再次覆蓋應該以尊重原始處理程序的方式進行,除非您想徹底替換它:

void(* original_zend_error_cb)(int類型,const char * error_filename,const uint error_lineno,const char * format,va_list args);void my_error_cb(int類型,const char * error_filename,const uint error_lineno,const char * format,va_list args){

    //個人特殊錯誤處理

 

    original_zend_error_cb(type,error_filename,error_lineno,format,args);}PHP_MINIT_FUNCTION(my_extension){

    original_zend_error_cb = zend_error_cb;

    zend_error_cb = my_error_cb;

 

    return SUCCESS;}PHP_MSHUTDOWN(my_extension){

    zend_error_cb = original_zend_error_cb;}

 

該掛鉤主要用於爲異常跟蹤或應用程序性能管理軟件實施集中式異常跟蹤。

引起異常時的通知

每當PHP Core或Userland代碼引起異常時,都會調用zend_throw_exception_hook並將異常做爲參數。

這個鉤子的簽名很是簡單:

void my_throw_exception_hook(zval * exception){

    if(original_zend_throw_exception_hook!= NULL){

        original_zend_throw_exception_hook(exception);

    }}

該掛鉤沒有默認實現,若是未被擴展覆蓋,則指向NULL。

static void(* original_zend_throw_exception_hook)(zval * ex);void my_throw_exception_hook(zval * exception);PHP_MINIT_FUNCTION(my_extension){

    original_zend_throw_exception_hook = zend_throw_exception_hook;

    zend_throw_exception_hook = my_throw_exception_hook;

 

    return SUCCESS;}

 

若是實現此掛鉤,請注意不管是否捕獲到異常,都會調用此掛鉤。將異常臨時存儲在此處,而後將其與錯誤處理程序掛鉤的實現結合起來以檢查異常是否未被捕獲並致使腳本中止,仍然有用。

實現此掛鉤的用例包括調試,日誌記錄和異常跟蹤。

掛接到eval()

PHPeval不是內部函數,而是一種特殊的語言構造。所以,您沒法經過zend_execute_internal或經過覆蓋其函數指針來鏈接它。

掛鉤到eval的用例並很少,您能夠將其用於概要分析或出於安全目的。若是更改其行爲,請注意可能須要評估其餘擴展名。一個示例是Xdebug,它使用它執行斷點條件。

extern ZEND_API zend_op_array *(* zend_compile_string)(zval * source_string,char * filename);

掛入垃圾收集器

當可收集對象的數量達到必定閾值時,引擎自己會調用gc_collect_cycles()或隱式地觸發PHP垃圾收集器。

爲了使您瞭解垃圾收集器的工做方式或分析其性能,能夠覆蓋執行垃圾收集操做的函數指針掛鉤。從理論上講,您能夠在此處實現本身的垃圾收集算法,可是若是有必要對引擎進行其餘更改,則這可能實際上並不可行。

int(* original_gc_collect_cycles)(無效);int my_gc_collect_cycles(無效){

    original_gc_collect_cycles();}PHP_MINIT_FUNCTION(my_extension){

    original_gc_collect_cycles = gc_collect_cycles;

    gc_collect_cycles = my_gc_collect_cycles;

 

    return SUCCESS;}

 

覆蓋中斷處理程序

當執行器全局EG(vm_interrupt)設置爲1時,將調用一次中斷處理程序。在執行用戶域代碼期間,將在常規檢查點對它進行檢查。引擎使用此掛鉤經過信號處理程序實現PHP執行超時,該信號處理程序在達到超時持續時間後將中斷設置爲1。

當更安全地清理或實現本身的超時處理時,這有助於將信號處理推遲到運行時執行的後期。經過設置此掛鉤,您不會意外禁用PHP的超時檢查,由於它具備自定義處理的優先級,該優先級高於對zend_interrupt_function的任何覆蓋。

ZEND_API void(* original_interrupt_function)(zend_execute_data * execute_data);void my_interrupt_function(zend_execute_data * execute_data){

    if(original_interrupt_function!= NULL){

        original_interrupt_function(execute_data);

    }}PHP_MINIT_FUNCTION(my_extension){

    original_interrupt_function = zend_interrupt_function;

    zend_interrupt_function = my_interrupt_function;

 

    return SUCCESS;}
相關文章
相關標籤/搜索