PHP系列之鉤子

it阿布發表於2020-07-29

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 引擎也可以生成更優化的函式呼叫操作碼。

這些掛鉤對效能非常敏感,具體取決於原始函式封裝程式碼的複雜性。

覆蓋內部功能

在覆蓋執行鉤子時,擴充套件可以記錄每個函式呼叫,你還可以覆蓋使用者域,核心和擴充套件函式(和方法)的各個函式指標。如果擴充套件僅需要訪問特定的內部函式呼叫,則具有更好的效能特徵。

#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;}

 

相關文章