不少打仗过 PHP 断点调试的一定都用过鼎鼎大名的 XDebug 。
不过我们本日讲的并不是这款扩展,而是另一个已经集成到 PHP 官方源码中的调试工具,并且,最主要的是,它调试时看到的内容是更为底层的 opcode 实行过程。
话不多说,我们直接进入到 phpdbg 这款工具的学习中吧!

phpdbg 命令行功能

在我们安装好 PHP 后,默认就有了 phpdbg 这个工具。
直接在命令走运行就会进入这个工具。

%phpdbg[Welcometophpdbg,theinteractivePHPdebugger,v0.5.0]Togethelpusingphpdbgtype"help"andpressenter[Pleasereportbugsto<http://bugs.php.net/report.php>]

没错,它便是随 PHP 安装的时候默认自带的,如果你的环境变量中没有这个工具命令的话,可以在 PHP 安装目录的 bin/ 目录下面找到。

phpphpdbgPHPDebug互动扩大phpdbg功效浅析 React

在进入 phpdbg 环境后,我们利用 help 就可以查看它的操作解释。

prompt>helpphpdbgisalightweight,powerfulandeasytousedebuggingplatformforPHP5.4+Itsupportsthefollowingcommands:InformationlistlistPHPsourceinfodisplaysinformationonthedebugsessionprintshowopcodesframeselectastackframeandprintastackframesummarygeneratorshowactivegeneratorsorselectageneratorframebackshowsthecurrentbacktracehelpprovidehelponatopic……

帮忙文档非常长,大家可以自己查看详细的内容,个中有一个 help 命令可以让我们看到许多简写的命令,我们紧张利用这些简写的命令别名就可以。

prompt>helpaliasesBelowarethealiased,shortversionsofallsupportedcommandseexecsetexecutioncontextsstepstepthroughexecutionccontinuecontinueexecutionrrunattemptexecutionuuntilcontinuepastthecurrentl

命令的简介和查看都很大略,那么我们要如何来调试 PHP 文件呢?这个才是我们最关心的事情。
在调试一个文件的时候,我们须要将它载入到当前的实行环境中。
可以在当前 phpdbg 环境中利用 e 命令指定文件进行载入,也可以在运行 phpdbg 的时候通过 -e 来指定须要载入的文件。

%phpdbg-ePHPDebug互动扩展.php[Welcometophpdbg,theinteractivePHPdebugger,v0.5.0]Togethelpusingphpdbgtype"help"andpressenter[Pleasereportbugsto<http://bugs.php.net/report.php>][Successfulcompilationof/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]prompt>

这里我们利用的是第二种办法,在启动 phpdbg 时利用 -e 参数来指定须要载入的文件。

普通断点设置

载入了文件,进入了命令行,我们就可以进行断点调试了。
首先,我们利用代码办法来设置断点。
在上面的测试文件中,我们利用下面的办法来定义断点。

echo111;phpdbg_break_file("PHPDebug互动扩展.php",3);echo222;phpdbg_break_file("PHPDebug互动扩展.php",6);

phpdbg_break_file() 函数便是来定义断点的,它有两个参数,第一个参数是文件名,这个不能乱填。
第二个参数是断点的行号。

接下来,在命令行中,我们运行两次简写的 run 命令 r 。

prompt>r111[Breakpoint#0addedat/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:3]222[Breakpoint#1addedat/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:6][Scriptendednormally]prompt>r[Breakpoint#0at/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:3,hits:1]>00003:echo111;00004:phpdbg_break_file("PHPDebug互动扩展.php",3);00005:prompt>

可以看出,在第一次运行 r 的时候, phpdbg 将全体文件进行了一次扫描并输出了当前的两个断点信息。
然后再运行一次 r 则定位到了第3行,也便是第一个断点的位置。
接下来,我们就要进行单步调试了,我们直策应用 step 的简写命令 s 。

prompt>s[L30x10ecae220ECHO111/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]111[L40x10ecae240EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]>00004:phpdbg_break_file("PHPDebug互动扩展.php",3);00005:00006:echo222;prompt>

断点位置向下运行了,果真是符合我们的预期,开始了一行一行的单步运行。
在上面输出的内容中,我们看到了 opcode 运行的状态。
比如 L3 0x10ecae220 ECHO 这一行指的便是第 3 行实行了 ECHO 操作。
是不是觉得非常高大上。

一起 s 下来,走到末了我们就结束了这次断点调试,phpdbg 环境将退出 run 运行时。

……prompt>s[Scriptendednormally]prompt>s[Notrunning]prompt>

这样,一趟调试就完成了。
当我们在第一个断点不想单步调试,想直接进入下一个断点,就可以利用 continue 的简写命令 c 来直接跳到下一个断点。

prompt>rDoyoureallywanttorestartexecution?(typeyorn):y[Breakpoint#0at/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:3,hits:1]>00003:echo111;00004:phpdbg_break_file("PHPDebug互动扩展.php",3);00005:prompt>c111[Breakpointat/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:3exists][Breakpoint#1at/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:6,hits:1]>00006:echo222;00007:phpdbg_break_file("PHPDebug互动扩展.php",6);00008:prompt>

其余还有一个命令便是可以直接看到当前载入的文件环境中的所有断点信息的。

prompt>infobreak------------------------------------------------FileBreakpoints:#0/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:3#1/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:6

以上,便是一个大略的行断点设置以及调试步骤。
当然,我们只是针对一个大略文件的测试,对付繁芜的框架型系统,断点的设置和调试就会繁芜很多,不过相应地,我们能够看到底层的 opcode 代码的实行情形,也能让我们对所测试的内容有更加深入的理解。

方法断点及运行步骤剖析

接下来我们来设置一个方法断点,并一步步不雅观察 opcode 的情形。

$i=1;//phpdbg-ePHP\Debug互动扩展.phpfunctiontestFunc(){global$i;$i+=3;echo"ThisistestFunc!i:".$i,PHP_EOL;}testFunc();phpdbg_break_function('testFunc');

在 PHP 代码中,我们利用 phpdbg_break_function() 来给这个 testFunc() 方法设置一个断点。
当代码中调用这个函数的时候,就会进入这个断点中。

prompt>r[Breakpoint#0intestFunc()at/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:11,hits:1]>00011:functiontestFunc(){00012:global$i;00013:$i+=3;prompt>s[L120x109eef620EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]>00012:global$i;00013:$i+=3;00014:echo"ThisistestFunc!i:".$i,PHP_EOL;prompt>s[L120x109eef640BIND_GLOBAL$i"i"/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php][L130x109eef660EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]>00013:$i+=3;00014:echo"ThisistestFunc!i:".$i,PHP_EOL;00015:}

直接进行了两次 s 单步,可以看到 global $i 对应的 opcode 操作是 BIND_GLOBAL 。
连续向下操作。

prompt>s[L130x109eef680ASSIGN_ADD$i3/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php][L140x109eef6a0EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]>00014:echo"ThisistestFunc!i:".$i,PHP_EOL;00015:}00016:prompt>s[L140x109eef6c0CONCAT"ThisistestFunc!"+$i~1/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php][L140x109eef6e0ECHO~1/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]ThisistestFunc!i:4[L140x109eef700EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php][L140x109eef720ECHO"\n"/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php][L150x109eef740EXT_STMT/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php]>00015:}00016:00017:testFunc();

第 13 行实行的是 $i += 3 的操作,对应的 opcode 操作是 ASSIGN_ADD ,增加的值是 3 。
连续 s 后实行了第 14 行,把稳这里进行了两步操作。
一次是 CONCAT ,一次是 ECHO ,然后代码正常输出了打印出来的语句。

从上面的几步调试可以清晰的看到 PHP 在 opcode 层面的一步步的实行状态,就像 XDebug 一样,每一次的实行都会有干系的变量、操作的信息输出。

类函数断点设置

类函数的断点设置实在就和上面的方法断点函数一样,非常的大略方便。

classA{functiontestFuncA(){echo"ThisisclassAtestFuncA!",PHP_EOL;}}$a=newA;$a->testFuncA();phpdbg_break_method('A','testFuncA');

这里就不贴出调试的代码了,大家可以自己考试测验一下。

命令行增加断点

除了在 PHP 代码中给出固定的断点之外,我们还可以在命令行中进行断点的增加,比如我们去掉之前的方法断点函数。
然后在命令行中指定在方法中增加一个断点。

prompt>btestFunc#3[Breakpoint#1addedattestFunc#3]

#3 这是什么意思呢?实在便是说我们在这个方法体内部的第 3 行增加一个断点。
也便是说,我们在 $i += 3; 这一行增加了一个断点。
行数是从方法定义那一行开始算的并且是从 1 开始,如果不加这个行数,便是直接从方法定义那一行开始。

prompt>r[Breakpoint#0resolvedattestFunc#3(opline0x1050ef660)][Breakpoint#0resolvedattestFunc#3(opline0x1050ef660)][Breakpoint#0resolvedattestFunc#3(opline0x1050ef660)][Breakpoint#0resolvedattestFunc#3(opline0x1050ef660)][Breakpoint#0intestFunc()#3at/Users/zhangyue/MyDoc/博客文章/dev-blog/php/202006/source/PHPDebug互动扩展.php:13,hits:1]>00013:$i+=3;00014:echo"ThisistestFunc!i:".$i,PHP_EOL;00015:}

实行 r 后,我们就直接定位到了 testFun() 方法中的第三行。

总结

本日我们只是大略的学习了一下 phpdbg 这个工具的利用。
从 help 命令中就可以看出,这个工具还有非常多的选项参数,可以帮我们完成许多调试事情。
在这里只是跟大家一起入个门,将来在学习的过程中再次打仗到的时候我们再连续深入的研究。

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/php/202006/source/PHPDebug%E4%BA%92%E5%8A%A8%E6%89%A9%E5%B1%95.php

参考文档:

https://www.php.net/manual/zh/intro.phpdbg.php