如果我们要在调用PHP
脚本里面的每一个函数前打印一句hello world
,大致有以下几种做法。(假设我们的扩展叫做observer
)
Hook zend_execute_ex
例如,我们可以在PHP
的模块初始化之前,进行如下的操作:
1 | void (*old_zend_execute_ex)(zend_execute_data *execute_data); |
但是,这种方式会有堆栈溢出的风险。
我们来看看,假设我们有如下的PHP
代码:
1 |
|
在没有hook zend_execute_ex
的情况下,执行结果如下:
1 | # 省略其他的打印 |
可以看到,这里调用了813538
次foo
函数,然后进程达到PHP
内存最大的限制而停下来了。
当我们把PHP
的内存限制设置到无限的时候,我们会发现,这个foo
函数可以无限的递归下去。为什么可以无限的递归下去呢?因为这种情况下,PHP有一个无限制的PHP
调用堆栈。
在hook
了zend_execute_ex
的情况下,执行结果如下:
1 | # 省略其他的打印 |
为什么这种情况下,就不能无限制的递归下去呢?因为当我们hook
了zend_execute_ex
之后,PHP
的函数调用都放在C
堆栈上面,也就意味着,受限于ulimit -s
的值。我机器默认的堆栈大小如下:
1 | ulimit -s |
这种情况下,我们只有调大系统的堆栈限制,才能解决堆栈被破坏的问题,例如我们扩大一倍的堆栈大小:
1 | ulimit -s 16384 |
再次执行脚本:
1 | # 省略其他的打印 |
可以看到,foo
函数调用次数基本上是成倍的增加了。
Hook opcode的handler
我们可以在ZendVM
执行函数调用的opcode
之前,执行一段我们的handler
:
1 | static int do_ucall_handler(zend_execute_data *execute_data) { |
这个实现方式也是可以无限制的递归调用foo
函数的,因为它仅仅用到了PHP
栈去实现foo
函数的调用。但是这种实现方式有一个问题,就是如何去处理opcode
的转发,我们可能会把其他扩展hook
的这个opcode
对应的handler
抹掉,从而导致一些出乎意料的问题。并且,这里还有一个很大的问题就是,和JIT
不兼容。使用Swoole
的小伙伴们应该知道,JIT
刚出来的时候,Swoole
下是无法开启JIT
的,就是因为Swoole
去Hook
了某些opcode
导致的。
observer api
这个是新一代的方式,也是目前PHP8
推荐的一种方式,这种即没有堆栈问题,也不会影响JIT
。
1 | static void observer_begin(zend_execute_data *execute_data) { |