这篇文章,我们来分析一下Xdebug
单步调试的原理。
一句话总结起来就是,Xdebug
利用ZEND_EXT_STMT
这个opcode
来实现了单步调试的功能。
那么,ZEND_EXT_STMT
这个opcode
是什么呢?大概可以这么理解,在执行一条语句之前,会执行ZEND_EXT_STMT
这个opcode
,这个opcode
不会对代码的执行结果造成影响,但是它可以帮助我们来实现调试器的功能。
举个例子,有如下的PHP
代码:
1 |
|
那么,它对应的opcode
为:
1 | [root@e2a14c00e7f6 test]# phpdbg test.php |
其中,L1-8
表示的是行数。我们发现,在执行每一条功能性的opcode
的时候,都会先执行一条ZEND_EXT_STMT
。
如果你没有开启Xdebug
,大概率是看不到这个EXT_STMT
的。也就是说,Xdebug
做了某些手脚,使得生成的opcode
里面包含了EXT_STMT
。我们可以来看一看在哪个地方对生成的opcode
进行了修改。
首先,我们得看一下PHP
内核是如何为生成的oparray
插入EXT_STMT
的:
1 | void zend_do_extended_stmt(void) /* {{{ */ |
通过zend_do_extended_stmt
这个函数来实现的。我们看到,只有当CG(compiler_options)
开启了ZEND_COMPILE_EXTENDED_STMT
标志,才会为oparray
插入EXT_STMT opcode
。
然后,我们发现,Xdebug
里面,就有添加ZEND_COMPILE_EXTENDED_STMT
标志的代码:
1 | PHP_RINIT_FUNCTION(xdebug) |
如果你把这一行代码注释掉,那么生成的opcode
就不会带有EXT_STMT
了,并且Xdebug
的单步调试功能会失效。
OK
,介绍完了ZEND_EXT_STMT
之后,我们来看一看他对应的handler
:
1 | static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_EXT_STMT_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS) |
我们发现,这个函数会去调用zend_extensions
的zend_extension_statement_handler
函数。而这个函数实际上就是xdebug_statement_call
,它在zend_extension_entry
里面被注册了。
xdebug_statement_call
这个函数做的事情就是阻塞读取客户端发来的命令。
所以,在客户端发来命令之前,是不会执行ZEND_EXT_STMT
后面的语句的。这就给了我们一种单步调试的感觉了。
明白了这个原理之后,我们完全可以自己写一个调试器了,有时间我写一个demo
出来给大家分享下。