PHP动态拦截函数调用需通过Zend扩展实现:一、Hook zend_execute_ex全局执行器;二、修改function_table中函数指针;三、OPCODE重写插桩(仅用户函数);四、借助runkit7或phpdbg运行时替换。

如果您希望在PHP运行时动态拦截特定函数的调用,例如监控、修改参数、记录日志或替换返回值,则需通过编写Zend扩展实现底层钩子。以下是几种可行的技术路径:
一、使用Zend API Hook zend_execute_ex
该方法通过替换全局执行器指针,在每次PHP函数调用前插入自定义逻辑,适用于拦截所有用户函数与内置函数(需配合函数名匹配)。其核心在于劫持执行流程入口,不修改OPCODE结构,兼容性较强。
1、在扩展的MINIT阶段保存原始zend_execute_ex函数指针。
2、定义新的execute_ex函数,在其中判断当前执行的函数名是否为目标函数,如为mysqli_query则执行拦截逻辑。
立即学习“PHP免费学习笔记(深入)”;
3、调用原始execute_ex继续执行,或直接构造zval返回伪造结果。
4、将新函数地址赋值给zend_execute_ex全局变量完成挂载。
二、修改function_table哈希表中的函数指针
此方式直接定位到Zend引擎中注册的函数结构体(zend_function),将其内部的op_array或handler字段重定向至自定义C函数,从而实现精确函数级替换。适用于已知函数名且无需影响其他函数的场景。
1、在RINIT阶段遍历EG(function_table)查找目标函数的zend_function结构。
2、确认该函数类型为ZEND_INTERNAL_FUNCTION或ZEND_USER_FUNCTION。
3、对ZEND_INTERNAL_FUNCTION,保存原function->internal_function.handler并替换为自定义handler。
4、在自定义handler中调用原handler前/后插入拦截代码,例如记录$_SERVER['REQUEST_URI']上下文。
三、利用OPCODE重写插桩(仅限用户函数)
针对用户定义函数,可在编译完成后、执行前遍历其op_array,向特定opcode(如ZEND_DO_FCALL)前插入自定义ZEND_INIT_USER_CALL或ZEND_DO_ICALL指令,间接实现调用前钩子。该方法不影响全局执行器,但无法拦截纯C实现的内置函数。
1、注册pass号为PHP_EXTENSION_PASS的op_array_handler回调。
2、在回调中检查op_array->function_name是否等于目标函数名,如file_get_contents。
3、分配新opline空间,在首条opcode前插入ZEND_EXT_STMT及自定义处理逻辑对应的opcode序列。
4、更新op_array->last和op_array->size,并调用zend_optimizer_add_literal注入常量参数。
四、基于phpdbg或runkit7的运行时函数替换
若不开发原生扩展,可借助现有扩展提供的API进行轻量级拦截。runkit7支持动态修改函数行为,phpdbg则提供执行断点能力,适合调试环境快速验证拦截逻辑。
1、启用runkit7扩展并确保runkit.internal_override=On。
2、调用runkit_function_redefine()将目标函数重映射至自定义PHP闭包。
3、在闭包内通过func_get_args()获取参数,执行日志记录后调用runkit_function_call()转发原始调用。
4、注意该方式会触发函数表重建,不可在高频调用路径中使用。











