当前位置: 首页 > 后端技术 > PHP

PHP系列的钩子

时间:2023-03-29 21:05:57 PHP

PHP提供的钩子PHP和ZendEngine为扩展提供了许多不同的钩子,允许扩展开发人员以PHPuserland无法提供的方式控制PHP运行时。本章将展示各种挂钩和从扩展挂钩到它们的常见用例。挂接到PHP功能的一般模式是PHP核心提供的扩展覆盖函数指针。然后扩展函数通常会完成自己的工作并调用原始的PHP核心函数。使用这种模式,不同的扩展可以覆盖同一个钩子而不会引起冲突。挂钩函数的执行Userland和内部函数的执行由Zend引擎中的两个函数处理,您可以用自己的实现替换它们。覆盖此挂钩的扩展的主要用例是通用函数级分析、调试和面向方面的编程。钩子在Zend/zend_execute.h中定义:ZEND_APIexternvoid(*zend_execute_ex)(zend_execute_data*execute_data);ZEND_APIexternvoid(*zend_execute_internal)(zend_execute_data*execute_data,zval*return_value);Minit这样做是因为ZendEngine中的其他决定是基于指针被覆盖这一事实提前做出的。覆盖的通常模式是这样的:staticvoid(*original_zend_execute_ex)(zend_execute_data*execute_data);staticvoid(*original_zend_execute_internal)(zend_execute_data*execute_data,zval*return_value);voidmy_execute_internal(zend_execute_data*valzvalue)*exe;(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;返回成功;}PHP_MSHUTDOWN_FUNCTION(my_extension){zend_execute_internal=original_zend_execute_internal;zend_execute_ex=original_zend_execute_ex;returnSUCCESS;}覆盖zend_execute_ex的一个缺点是它改变了Zend虚拟机运行时的行为以使用递归而不是在不离开解释器循环的情况下处理调用。此外,不覆盖zend_execute_ex的PHP引擎也可以生成更优化的函数调用操作码。这些挂钩对性能非常敏感,取决于原始函数包装代码的复杂性。重写内部函数重写执行挂钩时,扩展可以记录每个函数调用,您还可以重写用户空间、核心和扩展函数(和方法)的各个函数指针。如果扩展只需要访问特定的内部函数调用,则性能更好。#ifphp_version_id<70200typedefvoid(*zif_handler)(internal_function_parameters);#endifzif_handlerointern_handleronirstan_handler_handler_handler_handler_handler_var_dump;zend_nemed_nemed_nemed_function(my_overwrite_var_dump__ter_var_dumper_internal_ther_toper_internal_mir_parter_oilter_ointern_oinistion_oinistion_如果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;原始->内部函数n.handler=my_overwrite_pdo_exec;}}修改抽象语法树(AST)当PHP7编译PHP代码时,它首先将其转换为抽象语法树(AST),然后最终生成永久存储在Opcache中的操作码zend_ast_process钩子被每个编译的脚本调用并允许您在解析和创建AST之后修改它。这是使用起来最复杂的钩子之一,因为它需要对AST有完整的了解。在此处创建无效的AST可能会导致意外行为或崩溃。最好看一下使用此挂钩的示例扩展:GoogleStackdriverPHPDebuggerExtension基于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,则在Zend/zend_dtrace.c中使用dtrace_compile_file。Opcache将op数组存储在共享内存中以获得更好的性能,因此无论何时编译脚本,它的最终op数组都是从缓存中提供的,而不是重新编译。您可以在ext/opcache/ZendAccelerator.c中找到这个实现。名为compile_file的默认实现是Zend/zend_language_scanner.l中扫描器代码的一部分。实现此挂钩的用例是操作码加速、PHP代码加密/解密、调试或分析。您可以在执行PHP过程时随时替换此钩子,替换后编译的所有PHP脚本都将由该钩子的实现处理。始终调用原始函数指针非常重要,否则PHP将无法再编译脚本,Opcache将无法工作。扩展覆盖的顺序在这里也很重要,因为你需要知道你是想在Opcache之前还是之后注册钩子,因为如果Opcache在其共享内存缓存中找到操作码数组条目,它不会调用原始函数指针。Opcache将其挂钩注册为启动后挂钩,它在扩展的minit阶段之后运行,因此默认情况下在缓存脚本时将不再调用它。调用错误处理程序时的通知类似于PHPuserlandset_error_handler()函数,扩展可以通过实现zend_error_cb挂钩将自己注册为错误处理程序:char*格式,va_list参数);类型变量对应于E_*错误常量,这在PHP用户空间中也可用。PHP核心和用户态错误处理程序之间的关系很复杂:如果没有注册用户态错误处理程序,则始终调用zend_error_cb。如果注册了用户态错误处理程序,则始终会为E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR和E_COMPILE_WARNING的所有错误调用zend_error_cb挂钩。对于所有其他错误,仅当用户态处理程序失败或返回false时才会调用zend_error_cb。此外,由于Xdebug自身的复杂实现,它以不调用先前注册的内部处理程序的方式覆盖错误处理程序。因此,覆盖这个钩子不是很可靠。再次覆盖应该以尊重原始处理程序的方式完成,除非您想完全替换它:void(*original_zend_error_cb)(inttype,constchar*error_filename,constuinterror_lineno,constchar*format,va_??listargs);voidmy_error_cb(inttype,constchar*error_filename,constuinterror_lineno,constchar*format,va_??listargs){//我的特殊错误处理original_zend_error_cb(type,error_filename,error_lineno,format,args);}PHP_MINIT_FUNCTION(my_extension){original_zend_errorend_cb=orzend_error_cb=my_error_cb;returnSUCCESS;}PHP_MSHUTDOWN(my_extension){zend_error_cb=original_zend_error_cb;}这个钩子主要用于异常跟踪或者应用性能管理软件实现集中异常跟踪。抛出异常时的通知每当PHPCore或Userland代码抛出异常时,都会调用zend_throw_exception_hook并将异常作为参数。这个钩子的签名非常简单:}}此挂钩没有默认实现,如果未被扩展覆盖,则指向NULL。staticvoid(*original_zend_throw_exception_hook)(zval*ex);voidmy_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;returnSUCCESS;}如果实际发现这个挂钩,请注意无争论是否捕获到异常,将调用这个钩子。暂时将异常存储在这里仍然很有用,然后将其与错误处理程序挂钩的实现结合起来检查异常是否未被捕获并导致脚本停止。实现此挂钩的用例包括调试、日志记录和异常跟踪。连接到eval()PHPeval不是一个内在函数,而是一种特殊的语言结构。所以你不能通过zend_execute_internal或覆盖它的函数指针来连接它。挂钩eval的用例不多,您可以将其用于分析或安全目的。请注意,如果您更改其行为,可能需要评估其他扩展。一个例子是Xdebug,它使用它来执行断点条件。externZEND_APIzend_op_array*(*zend_compile_string)(zval*source_string,char*filename);hookintothegarbagecollector当可收集对象的数量达到一定阈值时,引擎本身会调用gc_collect_cycles()或者隐式触发PHP垃圾收集器。为了让您了解垃圾收集器的工作原理或分析其性能,您可以覆盖执行垃圾收集操作的函数指针挂钩。理论上,您可以在这里实现自己的垃圾收集算法,但如果需要对引擎进行其他更改,这在实践中可能不可行。int(*original_gc_collect_cycles)(无效);intmy_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;returnSUCCESS;}外壳中断处理程序当执行器全局EG(vm_interrupt)设置为1时,中断处理程序将被调用一次。在执行用户态代码期间,它会在定期检查点进行检查。引擎使用此挂钩通过信号处理程序实现PHP执行超时,该信号处理程序在达到超时持续时间后将中断设置为1。这有助于将信号处理推迟到运行时执行的后期,此时清理或实现您自己的超时处理更安全。通过设置这个钩子,您不会意外地禁用PHP的超时检查,因为它具有比任何对zend_interrupt_function的覆盖的自定义处理优先级。ZEND_APIvoid(*original_interrupt_function)(zend_execute_data*execute_data);voidmy_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;returnSUCCESS;}更多学习内容,请访问码农到架构师的培养之路