PHP 후크

coldplay.xixi
coldplay.xixi앞으로
2020-07-28 16:47:453185검색

PHP 후크

PHP에서 제공하는 후크

PHP 및 Zend 엔진은 확장 개발자가 PHP 사용자 영역에서 제공할 수 없는 방식으로 PHP 런타임을 제어할 수 있도록 하는 다양한 확장 후크를 제공합니다.

이 장에서는 확장 후크의 다양한 후크와 일반적인 사용 사례를 보여줍니다.

PHP 기능에 연결하는 일반적인 패턴은 PHP 코어에서 제공하는 재정의 함수 포인터를 확장하는 것입니다. 그런 다음 확장 함수는 일반적으로 자체 작업을 수행하고 원래 PHP 핵심 함수를 호출합니다. 이 패턴을 사용하면 서로 다른 확장이 충돌을 일으키지 않고 동일한 후크를 재정의할 수 있습니다.

관련 학습 권장사항: 초보자부터 능숙한 PHP 프로그래밍

함수 실행에 푹 빠져 있음

사용자 영역 및 내부 함수의 실행은 Zend 엔진의 두 가지 함수로 처리되며, 이를 다음으로 대체할 수 있습니다. 이 두 가지 기능을 직접 구현해 보세요. 이 후크를 다루는 확장의 주요 사용 사례는 일반적인 기능 수준 프로파일링, 디버깅 및 관점 지향 프로그래밍입니다.

후크는 Zend/zend_execute.h에 정의되어 있습니다: 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

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

이러한 함수 포인터를 덮어쓰려면 Minit에서 해야 합니다. Zend 엔진의 다른 결정은 포인터가 포인터인지 여부에 따라 결정되기 때문입니다. 이 사실을 취재하는 것은 사전에 이루어집니다.

일반적인 재정의 패턴은 다음과 같습니다.

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

zend_execute_ex 재정의의 한 가지 단점은 인터프리터 루프 Next 처리 호출을 떠나는 대신 재귀를 사용하도록 Zend 가상 머신 런타임의 동작을 변경한다는 것입니다. 또한 zend_execute_ex를 재정의하지 않는 PHP 엔진은 더욱 최적화된 함수 호출 opcode를 생성할 수도 있습니다.

이 후크는 코드를 캡슐화하는 원래 기능의 복잡성에 따라 성능에 매우 민감합니다. 🎜🎜🎜🎜내부 함수 재정의🎜🎜 실행 후크를 재정의할 때 확장 프로그램은 함수 호출을 기록할 수 있으며 사용자 영역, 핵심 및 확장 기능(및 메서드)에 대한 개별 함수 포인터를 재정의할 수도 있습니다. 확장이 특정 내부 함수 호출에만 액세스해야 하는 경우 성능 특성이 더 좋습니다. 🎜
void my_throw_exception_hook(zval * exception){
    if(original_zend_throw_exception_hook!= NULL){
        original_zend_throw_exception_hook(exception);
    }}
🎜클래스 메서드를 재정의할 때 함수 테이블은 zend_class_entry에서 찾을 수 있습니다. 🎜
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;}
🎜🎜🎜추상 구문 트리(AST)를 수정하세요. 🎜🎜PHP 7이 PHP 코드를 컴파일할 때 먼저 AST(추상 구문 트리)로 변환한 후 최종적으로 Opcache에 유지되는 opcode를 생성했습니다. zend_ast_process 후크는 모든 컴파일된 스크립트에 의해 호출되며 이를 통해 구문 분석 및 생성된 AST를 수정할 수 있습니다. 🎜🎜이것은 AST에 대한 완전한 이해가 필요하기 때문에 사용하기 가장 복잡한 후크 중 하나입니다. 여기서 잘못된 AST를 생성하면 예기치 않은 동작이나 충돌이 발생할 수 있습니다. 🎜🎜다음 후크를 사용하여 샘플 확장을 살펴보는 것이 좋습니다. 🎜
  • Google Stackdriver PHP 디버거 확장
  • AST를 사용한 Stackdriver 기반 개념 증명
🎜🎜 🎜스크립트/파일 컴파일에 익숙함🎜🎜사용자 스크립트가 include/require 또는 해당 include_once/require_once 를 호출할 때마다 code>이면 PHP 커널은 zend_compile_file 포인터에서 이 함수를 호출하여 이 요청을 처리합니다. 인수는 파일 핸들이고 결과는 zend_op_array입니다. 🎜
extern ZEND_API zend_op_array *(* zend_compile_string)(zval * source_string,char * filename);
🎜이 후크를 구현하는 PHP 코어에는 dtrace와 opcache라는 두 가지 확장이 있습니다. 🎜🎜 - 환경 변수 USE_ZEND_DTRACE를 사용하여 PHP 스크립트를 시작하고 dtrace 지원으로 PHP를 컴파일하는 경우 dtrace_compile_fileZend/zend_dtrace.c . <br>-Opcache는 더 나은 성능을 위해 공유 메모리에 op 배열을 저장하므로 스크립트가 컴파일될 때마다 최종 op 배열은 재컴파일되지 않고 캐시에서 제공됩니다. 이 구현은 <code>ext/opcache/ZendAccelerator.c에서 찾을 수 있습니다.
- compile_file이라는 기본 구현은 Zend/zend_언어_scanner.l에 있는 스캐너 코드의 일부입니다. 🎜🎜이 후크를 구현하는 사용 사례로는 Opcode 가속, 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_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_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;}

##替换操作码处理程序

TODO

위 내용은 PHP 후크의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 learnku.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제