<p> 虽然您可以使用 PHP 为系统管理和传统数据处理之类的任务创建命令行脚本,但是编程语言对 Web 应用程序的性能有主要影响。在使用过程中,每个 PHP 应用程序都驻留在服务器上,并且将通过代理(例如 Apache)调用 PHP 应用程序处理到来的请求。对于每个请求,典型的 PHP Web 应用程序在简短运行后将得到一个 Web 页面或 XML 数据结构。</p><p> 假定经过简单的运行后,一个分层构造的 Web 应用程序 ―― 包括客户机、网络、HTTP 服务器、应用程序代码和底层数据库 ―― 将会很难隔离 PHP 代码中的错误。即使假定除了 PHP 代码以外所有层都可以正常运行,跟踪 PHP 代码中的错误也会非常难,尤其是在应用程序利用较多的类时更是如此。</p><p> PHP 语句 echo 和函数 var_dump()、debug_zval_dump() 和 print_r() 都是常见且流行的调试辅助工具,可以帮助解决多种问题。但是,这些语句 ―― 甚至更健壮的工具,例如 PEAR Log package ―― 都是取证工具,必须在上下文环境之外先进行推测分析才能生成证据。</p><p> 在某种程度上,通过推论进行调试是一种蛮干的做法。收集并筛选数据,尝试推论出发生的问题。如果缺少重要信息,则必须重新测试代码、重复执行步骤,然后重新开始研究。一种更加高效的方法是在 程序运行时探测应用程序。您可以对请求参数分类,筛选过程调用堆栈,并查询任何所需的变量或对象。您可以暂时中断应用程序并且可以在变量更改值时收到警报。在某些情况下,您可以通过交互式询问 “如果……会怎样?” 问题来实际影响变量。</p><p> 称为调试器 的特殊应用程序支持这种 “实时的” 或交互式的检查。调试器可能启动并连接到进程上以便控制进程并监测其内存。或者,在使用解释语言的情况下,调试器可以直接解释代码。典型的现代图形化调试器可以索引并浏览代码,以符合人类阅读习惯的形式轻松地显示复杂的数据结构,并同时显示程序状态,如调用堆栈、中间输出和所有变量的值。例如,调试器通常都会把类的属性和方法分类并进行描述。</p>
<p> </p>
<p> 在本文和下一篇文章中,我将介绍的工具一定能够简化 PHP 调试。下一次,我将主要介绍交互式调试和 Zend Debugger ―― 一个特别针对 PHP 的健壮调试器 ―― 并探究它提供的许多功能。(Zend Debugger 是一款商业产品,是 Zend PHP 集成开发环境(IDE)的一部分)。我还将介绍一款开源 PHP 调试器,以免您只愿把钱花在啤酒上,而不是花在代码上。但是,本文将主要介绍如何更好地取证。</p><p> 类似《犯罪现场调查》,只是更令人讨厌</p><p> 代码出错、未能生成某个所需结果或者彻底崩溃时,您需要回答四个 w 问题:where、what、why 和 when: </p><p> “where” 是应用程序最后一次正常运行时所在的文件和行号。 </p><p> “what” 是犯错的代码 ―― 比如说,嫌疑犯。 </p><p> “why” 是错误的本质。可能它是一个逻辑错误和/或与操作系统进行交互所导致的错误,或两者兼具。 </p><p> 而 “when” 是出现错误时的上下文。在程序终止前发生了什么情况?像在所有犯罪行为中一样,如果您可以收集到足够的线索,那么线索就可以帮助您找到犯人。 </p><p> 一种取证工具 Xdebug(上一篇文章中使用的工具,用于分析 PHP 应用程序性能),如名称所示,将提供几个说明程序状态的功能,并且是应当添加到指令系统中的价值颇高的研究工具。安装后,Xdebug 将阻止无限次递归(表面上是这样)、修正关于堆栈跟踪和函数跟踪的错误消息以及监视内存分配,并提供其他功能。Xdebug 还包括一组函数,您可以将这组函数添加到代码中以进行运行时错误诊断。</p><p> 例如,下面的代码将使用一些 xdebug_...() 步骤测试 callee() 函数,以便输出调用程序的具体位置,包括文件名、行号和调用函数的名称。</p>
<p> </p>
<p> 清单 1. 测试 callee() 函数的步骤</p><p><code><?php<br /> function callee( $a ) {<br /> echo sprintf("callee() called @ %s: %s from %s",<br /> xdebug_call_file(),<br /> xdebug_call_line(),<br /> xdebug_call_function()<br /> );<br /> }<br /> $result = callee( "arg" );<br />?></code></p><p> 这段代码将生成:</p><p><code>callee() called @ /var/www/catalog/xd.php: 10 from {main}</code></p><p> 构建和安装 Xdebug</p><p> Xdebug 可以很轻松地从 UNIX® 类操作系统(包括 Mac OS X)中的源代码构建。如果是在 Microsoft® Windows® 上使用 PHP,则可以从 Xdebug Web 站点下载最新 PHP 版本的二进制 Xdebug 模块。</p><p> 让我们来构建和安装适用于 Debian “Sarge” Linux® 和 PHP V4.3.10-19 的 Xdebug。在撰写本文时,Xdebug 的最新版本是 V2.0.0RC4,发布于 2007 年 5 月 17 日。要继续本文,必须拥有 phpize 和 php-config 实用程序,并且必须能够编辑系统的 php.ini 配置文件(如果没有实用程序,请访问 PHP.net 以获得如何从头构建 PHP 的源代码和说明)。请执行以下步骤:</p><p> 下载 Xdebug tarball(一个用 gzip 压缩的 .tar 归档文件)。wget 命令可以帮助您轻松地完成此操作:<code>$<br />wget http://www.xdebug.org/files/xdebug-2.0.0RC4.tgz</code></p><p> 解压缩该 tarball 并切换到源代码目录:<code>$ tar xzf xdebug-2.0.0RC4.tgz<br />$ cd xdebug-2.0.0RC4</code></p><p> 运行 phpize 以准备适用于您的 PHP 版本的 Xdebug 代码:<code>$ phpize<br />Configuring for:<br />PHP Api Version: 20020918<br />Zend Module Api No: 20020429<br />Zend Extension Api No: 20021010</code></p>
<p> </p>
<p> phpize 的输出是一个脚本 ―― 通常名为配置 ―― 用于调整其余的构建过程。</p><p> 运行配置脚本:<code>$ ./configure<br />checking build system type... i686-pc-linux-gnu<br />checking host system type... i686-pc-linux-gnu<br />checking for gcc... gcc<br />checking for C compiler default output file name... a.out<br />checking whether the C compiler works... yes<br />checking whether we are cross compiling... no<br />checking for suffix of executables...<br />checking for suffix of object files... o<br />...<br />checking whether stripping libraries is possible... yes<br />appending configuration tag "F77" to libtool<br />configure: creating ./config.status<br />config.status: creating config.h</code></p><p> 通过运行 make 构建 Xdebug 扩展:<code>$ make<br />/bin/sh /home/strike/tmp/xdebug-2.0.0RC4/libtool<br />--mode=compile gcc -I.<br />-I/home/strike/tmp/xdebug-2.0.0RC4 -DPHP_ATOM_INC<br />-I/home/strike/tmp/xdebug-2.0.0RC4/include<br />-I/home/strike/tmp/xdebug-2.0.0RC4/main<br />-I/home/strike/tmp/xdebug-2.0.0RC4<br />-I/usr/include/php4 -I/usr/include/php4/main<br />-I/usr/include/php4/Zend -I/usr/include/php4/TSRM<br />-DHAVE_CONFIG_H -g -O0 -c<br />/home/strike/tmp/xdebug-2.0.0RC4/xdebug.c -o<br />xdebug.lo mkdir .libs<br />...<br />Build complete.<br />(It is safe to ignore warnings about tempnam and tmpnam).</code></p><p> 使用 make 将生成 Xdebug 扩展 xdebug.so。</p>
<p> </p>
<p> 安装该扩展:<code>$ sudo make install<br />Installing shared extensions: /usr/lib/php4/20020429/</code></p><p> 继续之前,使用鼠标选择并复制上一条命令显示的目录。该路径对于最后一步配置扩展至关重要。</p><p> 在您喜欢的文本编辑器中打开 php.ini 文件,然后添加以下代码:<code>zend_extension = /usr/lib/php4/20020429/xdebug.so<br />xdebug.profiler_enable = Off<br />xdebug.default_enable = On</code></p><p> 第一行将装入 Xdebug 扩展;第二行将禁用 Xdebug 的分析器功能(只是为了简单起见),而第三行将启用扩展的调试功能。 </p><p> 要检验 Xdebug 扩展是否已经安装并启用,请重新启动 Web 服务器,然后用代码 <?php phpinfo(); ?> 创建简单的一行 PHP 应用程序。如果将浏览器指向文件 ―― 如 http://localhost/phpinfo.php ―― 并向下滚动,您应当会看到类似图 1 所示的内容。</p><p> 图 1. 检验 Xdebug 扩展是否已经安装并运行</p><p> <img src="/content/uploadfile/200805/2008053117223014.jpg" onclick="get_larger(this)" alt="用Xdebug修正PHP应用程序中的错误" /></p><p> 注:如果您在 phpinfo() 的输出中没有看到 Xdebug 部分,则 Xdebug 装入失败。Apache 错误日志会列出原因。常见错误包括 zend_extension 的路径错误或者与其他扩展发生冲突。例如,如果需要使用 XCache 和 Xdebug,一定要先装入 XCache。但是,由于 Xdebug 适于在开发时使用并假定 xdebug.so 的路径正确,因此需要禁用其他扩展并重试。然后您可以重新启用扩展以执行其他测试,如缓存的效果。Xdebug 站点还有其他一些故障检修技巧。</p><p> 配置 Xdebug</p><p> 指令(图 1 中大表的最左侧一列)是一些可以设定的参数,用于改变 Xdebug 扩展的行为。可在 php.ini 文件中设置所有指令。一些指令用于配置调试工具;其他指令用于调整分析器的操作。忽略后者,让我们用一些合理设置来配置 Xdebug 以帮助调试 PHP 代码。</p>
<p> </p>
<p> 限制递归</p><p> 如果应用程序使用递归 ―― 例如,计算斐波纳契数列 ―― 并且终端环境不正确,应用程序会运行很长一段时间后才用尽内存或超时。您可以设定 xdebug.max_nesting_level 参数来限定递归深度。例如,xdebug.max_nesting_level = 50 将把递归深度限定为 50 次嵌套调用,然后将强制终止应用程序。下面演示一下,在启用 Xdebug 的状态下运行下列代码:</p><p> 清单 2. 限制递归</p><p><code><?php<br /> function deep_end( ) {<br /> deep_end();<br /> }<br /> <br /> deep_end();<br />?></code></p><p> 函数 deep_end() 将逐行进行到最底部。Xdebug 将在 49 次函数调用后介入并得到图 2(顺便说一句,main() 的初始调用用于启动程序计数作为第 1 次调用)。</p><p> 图 2. 如果调用堆栈超出限制,Xdebug 将终止执行</p><p> <img src="/content/uploadfile/200805/2008053117223231.jpg" onclick="get_larger(this)" alt="用Xdebug修正PHP应用程序中的错误" /></p><p> 如果应用程序大量使用递归隔离并解决较大的问题,则需要把深度相应地设定得 “更低”。否则,将 xdebug.max_nesting_level 设为较小的值,这样可以更快速地捕捉失控的函数调用序列。</p><p> 回答四个 w 问题</p><p> 出错时,您需要回答四个 w 问题。Xdebug 可以立即提供所有这些信息。下面是一些有益的初始设置;您可以随时调整这些设置。</p><p> 清单 3. 错误</p><p><code>xdebug.dump_once = On<br />xdebug.dump_globals = On<br />xdebug.dump_undefined = On<br />xdebug.dump.SERVER = REQUEST_METHOD,REQUEST_URI,HTTP_USER_AGENT<br />xdebug.dump.REQUEST=*<br />xdebug.show_exception_trace = On<br />xdebug.show_local_vars = 1<br />xdebug.var_display_max_depth = 6</code></p>
<p> </p>
<p> xdebug.dump_once、xdebug.dump_globals、xdebug.dump_undefined 和 xdebug.dump_SUPERGLOBAL 设置(其中 SUPERGLOBAL 可以是 COOKIE、FILES、GET、POST、REQUEST、SERVER 或 SESSION)用于控制哪些 PHP 超全局变量将被包含在所有诊断结果中。</p><p> 将 xdebug.dump_globals 设为 On 以转储名为 xdebug.dump_SUPERGLOBAL 设置中的超全局变量。例如,xdebug.dump_SERVER = REQUEST_METHOD,REQUEST_URI,HTTP_USER_AGENT 将打印 PHP 超全局变量 $_SERVER['REQUEST_METHOD']、$_SERVER['REQUEST_URI'] 和 $_SERVER['HTTP_USER_AGENT']。如果需要打印超全局变量数组中的所有值,请使用星号 (*),例如 xdebug.dump_REQUEST=*。如果进一步将 xdebug.dump_undefined 设为 On 并且不设定指定的超全局变量,则仍用值 undefined 打印变量。</p><p> 即使捕捉到异常,代码行 xdebug.show_exception_trace = On 仍将强制执行异常跟踪。代码行 xdebug.show_local_vars = 1 将打印每个函数调用的最外围中的所有局部变量,包括尚未初始化的变量。而 xdebug.var_display_max_depth = 6 表示转储复杂变量的深度。</p><p> 整合</p><p> 清单 4 显示了 php.ini 文件的 Xdebug 的所有相关设置。</p><p> 清单 4. php.ini 文件的设置</p><p><code>zend_extension = /usr/lib/php4/20020429/xdebug.so<br />xdebug.default_enable = On<br />xdebug.show_exception_trace = On<br />xdebug.show_local_vars = 1<br />xdebug.max_nesting_level = 50<br />xdebug.var_display_max_depth = 6<br />xdebug.dump_once = On<br />xdebug.dump_globals = On<br />xdebug.dump_undefined = On<br />xdebug.dump.REQUEST = *<br />xdebug.dump.SERVER = REQUEST_METHOD,REQUEST_URI,HTTP_USER_AGENT</code></p>
<p> </p>
<p> 将这些设置(或类似的内容)保存到 php.ini 文件中,然后重新启动 Web 服务器。</p><p> 解释转储报告</p><p> 以下示例显示了出错时发生的情况。把您的 “有待改进” 的代码修改为类似清单 5 所示的代码。</p><p> 清单 5. 修改错误代码</p><p><code><?php<br /> function deep_end( $count ) {<br /> // add one to the frame count<br /> $count += 1;<br /> if ( $count < 48 ) {<br /> deep_end( $count );<br /> }<br /> else {<br /> trigger_error( "going off the deep end!" );<br /> }<br /> }<br /> // main() is called to start the program,<br /> // so the call stack begins with one frame<br /> deep_end( 1 );<br />?></code></p><p> 如果运行这段新代码,您应当会看到大量信息,如下所示:</p><p> 图 3. 出错时超全局变量、堆栈和局部变量的转储</p><p> <img src="/content/uploadfile/200805/2008053117223231.jpg" onclick="get_larger(this)" alt="用Xdebug修正PHP应用程序中的错误" /></p><p> 传递给 trigger_error 的消息文本显示在顶部。底部是受请求的 $_SERVER 元素列表和已经定义的 $_REQUEST 元素列表。最底部是 #48 范围中的变量列表,这是根据清单对 deep_end() 进行的调用。在调用中,$count 是整数 48。当此 Xdebug 配置就绪后,您现在有更多的线索可以跟踪犯罪者。</p><p> 下面是另外一个技巧:Xdebug 提供了一个增强型 var_dump() 函数,它对于 PHP 数组和类尤为有帮助。例如,清单 6 显示了简单的(PHP V4)类和实例。</p><p> 清单 6. PHP V4 类和实例</p>
<p> </p>
<p><code><?php<br /> class Person {<br /> var $name;<br /> var $surname;<br /> var $age;<br /> var $children = array();<br /> function Person( $name, $surname, $age, $children = null) {<br /> $this->name = $name;<br /> $this->surname = $surname;<br /> $this->age = $age;<br /> foreach ( $children as $child ) {<br /> $this->children[] = $child;<br /> }<br /> }<br /> } <br /> $boy = new Person( 'Joe', 'Smith', 4 );<br /> $girl = new Person( 'Jane', 'Smith', 6 );<br /> $mom = new Person( 'Mary', 'Smith', 34, array( $boy, $girl ) );<br /> var_dump( $boy, $mom );<br />?></code></p><p> 清单 7 显示了 var_dump() 的输出。</p><p> 清单 7. var_dump() 输出</p><p><code>object(person)<br /> var 'name' => string 'Joe' (length=3)<br /> var 'surname' => string 'Smith' (length=5)<br /> var 'age' => int 4<br /> var 'children' =><br /> array<br /> empty<br /> <br />object(person)<br /> var 'name' => string 'Mary' (length=4)<br /> var 'surname' => string 'Smith' (length=5)<br /> var 'age' => int 34<br /> var 'children' =><br /> array<br /> 0 =><br /> object(person)<br /> var 'name' => string 'Joe' (length=3)<br /> var 'surname' => string 'Smith' (length=5)<br /> var 'age' => int 4<br /> var 'children' =><br /> array<br /> empty<br /> 1 =><br /> object(person)<br /> var 'name' => string 'Jane' (length=4)<br /> var 'surname' => string 'Smith' (length=5)<br /> var 'age' => int 6<br /> var 'children' =><br /> array<br /> empty</code></p>
<p> </p>
<p> 如果结合使用 Xdebug 与 PHP V5 类,转储包括 public、private 和 protected 之类的属性。</p><p> 跟踪代码</p><p> 解决错误 ―― 如解开神秘谋杀之谜 ―― 通常要求构造详细的时间线。例如,内存泄漏通常不会把自身表明为一个错误计算。相反,操作将正常进行,直至内存用尽,然后应用程序突然终止。如果内存泄漏由于某些请求而恶化,可能会不断出现错误并且难以预测。在内存使用量与时间之间建立映射的时间线将揭示泄漏的严重程度。一条精细的时间线 ―― 比如,从函数到函数 ―― 将进一步指出泄漏源。</p><p> Xdebug 可以提供一条详细的时间线进行执行跟踪。当跟踪被启用后,Xdebug 将记录所有函数调用,包括每个函数的参数和返回值。您可以将每个日志或跟踪 的格式设为符合人类阅读习惯或者机器可读的格式。您最好使用前者,虽然您可能编写独立而特定的应用程序来分析后者。</p><p> 同转储一样,Xdebug 有若干个 php.ini 选项用于自定义跟踪内容。例如,下面一批设置将生成最详细的输出。</p><p> 清单 8. 跟踪自定义</p><p><code>xdebug.trace_format = 0<br />xdebug.auto_trace = On<br />xdebug.trace_output_dir = /tmp/traces<br />xdebug.trace_output_name = trace.%c.%p<br />xdebug.collect_params = 4<br />xdebug.collect_includes = On<br />xdebug.collect_return = On<br />xdebug.show_mem_delta = On</code></p><p> 设定 xdebug.auto_trace = 1 将在执行所有 PHP 脚本之前先启用自动跟踪。另外,您可以通过代码设定 xdebug.auto_trace = 0,并分别使用 xdebug_start_trace() 和 xdebug_stop_trace() 函数启用和禁用跟踪。但是,如果 xdebug.auto_trace 为 1,则可以在包括配置好的 auto_prepend_file 之前先启动跟踪。</p>
<p> </p>
<p> 选项 xdebug.trace_ouput_dir 和 xdebug.trace_output_name 用于控制保存跟踪输出的位置。在这里,所有文件都被保存到 /tmp/traces 中,并且每个跟踪文件都以 trace 为开头,后接 PHP 脚本的名称(%s)以及进程 ID(%p)。所有 Xdebug 跟踪文件都以 .xt 后缀结尾。</p><p> 默认情况下,Xdebug 将显示时间、内存使用量、函数名和函数调用深度字段。如果将 xdebug.trace_format 设为 0,则输出将符合人类阅读习惯(将参数设为 1 则为机器可读格式)。此外,如果指定 xdebug.show_mem_delta = 1,则可以查看内存使用量是在增加还是在减少,而如果指定 xdebug.collect_params = 4,则可以查看传入参数的类型和值。要监视每个函数返回的值,请设定 xdebug.collect_return = 1。</p><p> 接下来看另外一个示例。创建 /tmp/traces 目录,然后用 mkdir /tmp/traces; chmod a+rwx /tmp/traces 将其模式更改为能够被任何用户阅读的文件(world-readable)和能够被任何用户写入的文件(world-writable)(如果您不愿共享 traces 目录,请确保至少 Web 服务器用户 ―― 通常为 www 或任何人 ―― 可以将数据写入该目录)。将以上跟踪设置添加到 php.ini 文件中,重新启动 Web 服务器,然后把浏览器再次指向 phpinfo() 应用程序。整个跟踪应当类似清单 9 所示:</p><p> 清单 9. 整个跟踪</p><p><code>TRACE START [2007-06-06 14:04:55]<br /> 0.0003 9440 +9440 -> {main}() /var/www/catalog/t/info.php:0<br /> 0.0005 9440 +0 -> phpinfo() /var/www/catalog/t/info.php:1<br /> >=-> TRUE<br /> >=-> 1<br /> 0.2351 9208<br />TRACE END [2007-06-06 14:04:55]</code></p>
<p> </p>
<p> 在这里,main() 将调用 phpinfo(),后者将返回 TRUE。当 main() 退出时,它将返回 1。接下来,将浏览器指向 “最复杂的内容” 或系统中的其他某个 PHP 应用程序以生成更详细的跟踪。</p><p> 清单 10 显示了在计算第四个斐波纳契数列时上一篇文章中的 PHP Fibonacci 生成器的跟踪:</p><p> 清单 10. PHP Fibonacci 生成器跟踪</p><p><code>TRACE START [2007-06-06 14:17:17]<br />0.0004 16432 +16432 -> {main}() /var/www/catalog/t/fibonacci.php:0<br />0.0006 16696 +264 -> fib('4') /var/www/catalog/t/fibonacci.php:35<br />0.0007 16696 +0 -> fib(3) /var/www/catalog/t/fibonacci.php:7<br />0.0007 16736 +40 -> fib(2) /var/www/catalog/t/fibonacci.php:7<br />0.0007 16848 +112 -> fib(1) /var/www/catalog/t/fibonacci.php:7<br />>=> 1<br />0.0008 16904 +56 -> fib(0) /var/www/catalog/t/fibonacci.php:7<br />>=> 0<br />>=> 1<br />0.0009 16904 +0 -> fib(1) /var/www/catalog/t/fibonacci.php:7<br />>=> 1<br />>=> 2<br />0.0009 16904 +0 -> fib(2) /var/www/catalog/t/fibonacci.php:7<br />0.0009 16904 +0 -> fib(1) /var/www/catalog/t/fibonacci.php:7<br />>=> 1<br />0.0010 16904 +0 -> fib(0) /var/www/catalog/t/fibonacci.php:7<br />>=> 0<br />>=> 1<br />>=> 3<br />>=> 1<br />0.0011 12528<br />TRACE END [2007-06-06 14:17:17]</code></p></p><p> 第一列显示时间,第二列是累计的内存使用量,第三列是增加的内存使用量,而第四列显示函数调用,包括参数。</p><p> 标有 >=> 的行显示每个函数的返回值(查找相应的缩进 -> 将调用与其返回值匹配起来)。此外,最后的 >=> 1 是 main() 的返回值。</p><p> 如果使用 vim,Xdebug 的创造者 Derick Rethans 提供了专门针对 Xdebug 跟踪的一组语法加亮提示。提示包含在 Xdebug 源代码包内的 xt.vim 文件中。对于最近的 Linux 发行版,只需将 xt.vim 复制到 $VIMRUNTIME/syntax/xt.vim 中,然后运行 vim tracefile.xt。图 4 显示了 vim 中加亮的 Fibonacci 跟踪。</p><p> 图 4. Xdebug 跟踪的 vim 语法文件将使您可以轻松地进行分析</p><p> <img src="/content/uploadfile/200805/2008053117223459.jpg" onclick="get_larger(this)" alt="用Xdebug修正PHP应用程序中的错误" /></p><p> 结束语</p><p> 跟踪 PHP 代码中的错误可能是一项挑战。但是如果您有开发系统并且可以安装 Xdebug,那么更正这些错误就会变得轻松得多。Xdebug 可以显示堆栈跟踪,转储甚为复杂的变量,随时间跟踪内存使用量,并允许您在出错或崩溃时(不是如果,而是发生时)进行有效的事后分析。</p></p>