来源:http://www.ibm.com/developerworks/cn/opensource/os-php-zenddebug/index.html

 

Martin Streicher, 主编, Linux Magazine

2007 年 11 月 26 日

调试器 是一种特殊的应用程序,它们可以探测正在运行的代码,允许任意地暂停执行、检查对象、检查调用堆栈,甚至在运行时修改变量的值。学习如何使用调试器纠正 PHP 代码中的 bug。

根据一个广为流传的传说,第一个计算机 bug 是一只真正的虫子,它出现在 Harvard University 测试的 Mark II Aiken Relay Calculator 中的一个继电器上。根据操作员在 1947 年 9 月 9 日所做的日志记录,这只虫子是 “(在计算机中)发现的第一个 bug”。在图 1 中可以看到这份手写的日志记录和这只声名狼籍的虫子。


图 1. 声名狼籍的 Mark II 虫子
声名狼籍的 Mark II 虫子

实际上,bug 这个词的起源要早得多,可能比前面这次事件早了大约七十年。在 1848 年,Thomas Edison 在描述机械故障时写道,“首先是觉得有点儿不对劲儿,然后是一声爆响,接着麻烦就来了 —— 机器不正常了,出现了小故障和各种麻烦等 ‘bug’...” 显然,与 Edison 同时代的人已经把 bug 这个词当作行话了。

了解到即使是 Edison 也必须对其发明进行 “调试(debug)”,软件开发人员可能会觉得有所慰藉并受到鼓舞(Edison 没有用过 debug 这个词。这个词比较新,它是在二战时期从航空工业开始成为工程师的行话的)。或者,您可能希望所有的 bug 都是真正的虫子,那倒好办了,我们只需安装一个捕虫器就行了。“计算机 bug 进得来,但是出不去。”(有人能在 Subversion 的下一版中增加这个特性吗?)

但是,正如 Edison 所指出的,bug 是每个工程项目的固有部分。在讨论发明时,Edison 写道,“必须经过数月的艰苦观察、研究和劳动,才能让产品经受市场的考验。”

幸运的是,软件开发人员可以利用工具简化 “观察” 的过程,将花费的时间从几个月减少到几分钟,至少减少到几小时或几天。以前的一篇文章 “Squash bugs in PHP applications with Xdebug” 介绍了收集分析故障原因所需的信息的各种技术。但是,这种故障后分析往往很困难,而且很耗费时间,因为必须做出推测,然后进行测试。如果缺少关键的信息,那么必须反复研究、调整和测试代码,这个过程可能要重复许多次。

本文讨论一种更高效的调试技术:交互式调试(interactive debug)。有一种称为调试器(debugger) 的特殊应用程序,它们可以探测正在运行的代码,允许在任意的断点暂停执行,检查对象、检查调用堆栈和环境,甚至允许在运行时修改变量的值。

在本文中,将使用 Zend Debugger;它是 Zend 引擎的一个扩展,可以探察正在运行的 PHP 应用程序。可以免费下载并使用 Zend Debugger。但是,为了控制 Zend Debugger 和查看它的诊断信息,需要同时使用一个客户机应用程序。客户机可以是从命令行运行的简单程序,也可以是成熟的集成开发环境(IDE),提供编辑器、代码补全、图形化类浏览器等特性。

有好几种开放源码客户机可以与 Zend Debugger 进行交互,包括开放源码的 PHP Eclipse 插件。但是,我喜欢的 PHP IDE 是由 Zend Technologies 提供的 Zend Studio,这家公司同时提供 PHP 运行时引擎的开放源码版本和商业版本。与 PHP 本身不同,Zend Studio 是一个商业产品。可以下载这个软件并免费使用 30 天,这段时间足够您体会它的众多特性了;但是在此之后,如果希望继续使用它,就必须购买许可证。对于我来说,许可证的费用绝对物有所值。

您应该试试各种客户机,寻找适合自己的软件。例如,许多 IDE 包含您喜欢的编辑器,这样就不需要重新学习一套键盘快捷键。无论选择哪种软件,当您开始使用调试器客户机之后,很可能会发现再也离不开它了。您可以和 print_r() 说再见了!

安装 Zend Studio 和 Zend Debugger

首先,下载并安装 Zend Studio 和 Zend Debugger 软件。这里的步骤说明针对 Mac OS X,但是在 Linux® 和 Microsoft® Windows® 操作系统上安装步骤是相似的。(Zend Studio Web 页面为每种平台提供了安装说明。)实际上,可以在本地系统上安装并运行 Zend Studio,将调试器部署在服务器上,这样就可以进行远程代码调试。

无论使用哪种平台,都要确保系统中运行着 PHP V4 或 PHP V5:Zend 软件可以与这两个版本的 PHP 配合工作。因为 Mac OS X 当前附带 PHP V4(更精确地说,是 V4.4.7),所以本文基于这个比较老的 PHP 版本。

安装 Zend 工具的步骤如下:

  1. 访问 Zend Downloads 并单击 Try for Zend Studio。
  2. 创建一个登录,然后下载 Zend Studio Client V5.5.0a(这是编写本文时的最新版本)和 Zend Debugger V5.2.6。
  3. 下载完成时,运行 Zend Studio 安装程序。安装程序运行结束之后,系统中应该有一个名为 /Applications/Zend Studio 5.5.0/ 的目录。
  4. 打开这个文件夹,再打开 bin 子文件夹,然后双击应用程序 ZDE。应该会出现与图 2 相似的空 IDE。

    图 2. Zend Studio IDE
    Zend Studio IDE

    稍后再设置这个 IDE 来安装调试器。

  5. 下载 Zend Debugger 软件并用 tar xzvf ZendDebugger-5.2.6-darwin8.6-uni.tar.gz 命令进行解压,创建 ZendDebugger-5.2.6-darwin8.6-uni 目录。
  6. 进入这个新目录并将文件 4_4_x_comp/ZendDebugger.so 复制到另一个便于寻找的目录,以防止意外删除这个文件。如果在您的 Mac 机上已经安装了 Xcode 开发工具,那么 /Developer/Extras/PHP 是放置这个文件的合适位置:
    $ sudo mkdir -p /Developer/Extras/PHP
    $ sudo chgrp admin /Developer/Extras/PHP
    $ sudo chmod g+rwx /Developer/Extras/PHP
    $ sudo cp 4_4_x_comp/ZendDebugger.so /Developer/Extras/PHP
    

    Zend Debugger 是一个 Zend 扩展,必须在 php.ini 中启用它。在 Mac OS X 中,php.ini 文件通常位于 /private/etc/php.ini。

  7. 编辑这个文件并添加以下代码行:
    zend_extension=/Developer/Extras/PHP/ZendDebugger.so
    zend_debugger.expose_remotely=allowed_hosts 
    zend_debugger.allow_hosts=127.0.0.1
    

    如果没有 php.ini 文件(这是正常的,这仅仅意味着 PHP 目前只使用默认的内部设置),那么创建一个包含以上代码的文件。第一行代码装载这个扩展。第二行将对调试器的连接限制在一个 IP 地址列表的范围内,这个列表在第三行中提供。同样,这里的 PHP(包含 Zend Debugger)在本地运行。所以这里使用 loopback 接口 127.0.0.1,但是在远程系统上运行 PHP 也很容易。如果可以从因特网访问这个系统,就要使用第二行和第三行限制对调试器的访问。

  8. 在 Mac OS X 系统上的 Apache HTTP Server 中启用 PHP 扩展。打开文件 /private/etc/httpd/httpd.conf,在以 LoadModule php4_module 开头的行和包含 AddModule mod_php4.c 的行中删除行首的 #
  9. 如果在 Mac OS X 系统上还没有运行 Apache HTTP Server,就启动它。
  10. 打开 System Preferences,选择 Sharing,然后启用 Personal Web Sharing,见 图 3。如果 Personal Web Sharing 已经启用了,就单击 Stop,然后单击 Start 重新启动 Web 服务器。

    图 3. 在 Mac OS X 系统上启用 Web 服务器
    在 Mac OS X 系统上启用 Web 服务器

为了检验 PHP 和 Web 服务器的运行是否正常,进入 ~/Sites/ 目录并创建包含以下内容的 info.php 文件:

<?php 
  phpinfo(); 
?>

在浏览器中访问 http://localhost/~username/info.php,其中的 username 是您的 Mac OS X 登录名。如果配置正确,应该会在 phpinfo() 的结果中看到标有 Zend Debugger 的一节。这一节还应该反映 php.ini 的设置。


图 4. 检验调试器是否正在工作
检验调试器是否正在工作

请注意 zend_debugger.connector_port 设置。在初始化调试会话时,应该连接 Web 服务器的这个端口。

顺便说一句,如果您觉得这个安装过程比较复杂,或者希望节省时间,那么可以下载并安装 Zend Core 或 Zend Platform。这两个软件是商业产品,但是都预先配置了 Zend Debugger。本文余下的说明对于 Zend Core 或 Zend Platform 同样是有效的。





回页首


连接 Zend Debugger

Zend Studio 和 Zend Debugger 的组合允许远程控制 Zend 引擎。Zend Studio 作为控制面板,显示关于引擎内部机制的信息。可以看到正在运行的源代码、变量的状态、Web 环境的状态以及其他数据。

Zend Debugger 像是一个代理:它将来自 Zend 引擎的信息转发给 Zend Studio,将来自 Zend Studio 的命令转发给 Zend 引擎。命令包括各种调试概念,比如 continue(恢复执行)、step(执行下一个语句)和 stop(停止执行)。每当 Zend 引擎的状态发生改变时,调试器将变化情况转发给 Zend Studio 以便显示。

因此,为了调试 PHP 应用程序,要让 Zend Studio 连接到调试器,然后在浏览器中访问应用程序。当应用程序开始运行时,调试器作为代理运行并将控制传递给 Zend Studio。这样开发人员就可以接管控制权了。

配置 Zend Studio

下面是一种连接 Zend Debugger 的简单方法(以后我会提供一个比较复杂的脚本并讨论更多调试技巧):

  1. 启动 Zend Studio IDE。
  2. 选择 Zend Studio > Preferences,然后单击 Debug 选项卡:
    • 在这个示例中,Web 服务器(其中包含 Zend Debugger)是与 Zend Studio 分离的,所以将 Debug Mode 设置为 Server
    • 对于本文,所有 PHP 应用程序都创建在您的主文件夹的个人 Web 站点目录中。所以指定 localhost/~username/ 作为 Debug Server URL(这可以是服务器上包含 PHP 文件的任何路径。使用这个设置和 Dummy File 设置在 Zend 引擎中启用调试)。我的 Mac OS X 用户名是 mstreicher,所以我指定 localhost/~mstreicher/,见 图 5
    • 调试器使用 Client IP 连接那个运行 Zend Studio 的系统。这个设置通常可以是 Default,但是因为客户机和服务器都在本地运行,所以选择 Customized 并提供 localhost 作为 IP 地址。
    • 可以将 Dummy File 设置为 Debug Server URL 路径中的任何 PHP 文件名。Dummy File 作为一个引导程序:每当启动调试会话时,URL Debug Server URL/Dummy File ?start_debug=1&debug_host= Client IP&... 配置和启用调试器。如果 Web 服务器无法找到 Dummy File,调试器就不起作用(如果在笔记本计算机上运行 Zend Studio,对远程服务器上的应用程序代码进行调试,那么一定要将 Dummy File 放在远程服务器上应用程序的文档根目录中)。要想检查 Dummy File 是否存在,只需在浏览器中访问它的 URL。如果没有看到错误,就说明配置是正确的。
    • Client Debug Port 设置为任何非特权端口,比如 10000。
    • Debug 选项卡上的所有其他设置可以保持默认值。完成设置时,窗口应该与图 5 相似。

      图 5. 连接 Zend Debugger 所需的设置
      连接 Zend Debugger 所需的设置

    • 单击 OK

现在,可以编写并调试 PHP 代码了。

调试一些简单的代码

Zend Studio 提供了编写 PHP 代码所需的所有工具:一个用来编辑文件的编辑器;一个文件管理器,用来将许多文件集中在一个项目中;一个作为参考的 PHP 类和模块浏览器;一个用来查看中间结果的输出视图;一个环境浏览器,可以直接查看变量、调用堆栈和输出缓冲区。更好的是,编辑器是上下文敏感的,可以自动补全 PHP 控制结构、关键字和字符串分隔符。

首先,在 Zend Studio 顶部中间的 Editor 窗口中单击鼠标。输入 清单 1 所示的代码片段,体会一下编辑器的代码补全特性。这段代码的意图是输出数字 1-10。


清单 1. 输出数字 1-10
                
<?php
    $counter = 1;
    
    while ( $counter < 10; ) {
        printf( "%d/n", $counter );
        
        increment( $counter );
    }
    
    function increment( $i ) {
        $i++;
        
        return;
    }
?>

输入完代码之后,单击工具栏中绿色的 Play 按钮运行代码。但是,代码并没有输出 1-10,而是不停地输出数字 1。单击红色的 Stop 按钮停止程序。

这里显然有 bug。首先要调查的是:$counter 发生了什么情况?为此,需要在第 7 行和第 11 行停止执行。在第一个断点上,可以检查 $counter 在调用 increment() 之前的值;在第二个断点上,可以检查传递的参数值以及局部变量 $i 的值。在第 7 行上设置断点:将游标移动到第 7 行上,然后选择 Debug > Add/Remove Breakpoint。第 7 行应该会显示为红色,表示这一行上设置了断点。对第 11 行执行相同的操作。如果想查看断点的列表,可以单击 Debug 窗口底部的 Breakpoints 选项卡。图 6 显示设置断点之后的 IDE 窗口。


图 6. 在代码示例中设置的断点
在代码示例中设置的断点

接下来,单击 Play 重新启动应用程序。程序在第 7 行停止执行。Debug Output 面板(在最右边)包含一些输出,Debug 窗口中的 Variables 选项卡显示变量的列表。数组标有方括号([]);简单的标量标有蓝色圆点。应该会看到 $counter = (int) 1

现在单击工具栏中蓝色的向下箭头,执行函数中的下一个语句(蓝色的向下箭头和短线的作用是调用函数,并前进到调用者中的下一个语句)。游标现在应该出现在第 11 行,Variables 选项卡应该只显示一个变量:形式参数 $i = (int) 1。到目前为止,一切正常。

再次单击向下箭头,执行第 11 行。$i 现在是 2。单击 Debug 窗口中的 Stack 选项卡,见图 7。


图 7. 堆栈跟踪
堆栈跟踪

再次单击向下箭头,返回第 5 行。让人吃惊的是,$counter 仍然是 1,这是第一个 bug 的根源。应该用变量的引用 调用 increment() 函数,而不是使用变量的值。这很容易纠正!只需将这个函数调用改为 increment( &$counter )

单击 Stop,然后再次单击 Play。分步执行这个程序,应该会看到 1, 2, 3, . . . 8, 9。怎么没有 10 呢?答案很明显:这是一个边界错误。只需将 < 改为 <=。如果删除原来的两个断点并在第 4 行上设置一个新断点,就可以看到这个错误是如何发生的。

尽管这个示例是人为制造的,但是它演示了几种基本的交互式调试技术:

  • 分步执行程序,每次执行一个命令,从而观察变量值并寻找出错的位置。
  • 如果在许多地方调用了一个函数,可以使用堆栈跟踪检查调用序列。常常可以发现 bug 的原因是调用者发送了类型错误的参数或数量错误的参数,或者实际参数和形式参数的次序不匹配。还可以展开跟踪中的条目(单击名称左边的向右小箭头),查看函数的形式参数的值。
  • 使用断点暂停程序的执行以便检查状态。例如,如果怀疑一个函数的代码有问题,那么在这个函数的第一个语句上设置断点,程序在此暂停执行,查看堆栈跟踪,然后分步执行这个函数来寻找 bug。
  • 使用 Debug 窗口中的 Variables 选项卡查看环境、全局和局部变量。

要想进一步了解 Zend Studio,可以使用 IDE 编写 PHP 代码并单击各个选项卡,从而研究它的众多特性。即使选择在另一个程序中编辑代码,也可以利用 Zend Studio 调试类、方法和单元测试。在 IDE 中编写单元测试尤其有帮助。





回页首


调试 PHP 应用程序

当然,PHP 调试器的真正价值在于实时探察 Web 应用程序。下面提供一个这样的场景。

比萨饼应用程序

清单 2清单 3 是一个简单的 PHP 应用程序,其中包含一个类、方法和一些包装器代码。清单 2(index.php)所示的应用程序使用 Pizza 类(清单 3)创建比萨饼订单,计算每个比萨饼的价格。代码并不复杂,但是其中有两个 bug,这足以演示调试 PHP 应用程序的方法。一个 bug 很明显:根据配料数量的不同,比萨饼的价格应该不一样,但是这个程序产生的价格都一样。第二个 bug 是一个惯例问题:同一种配料不应该两次计价。


清单 2. 比萨饼订单应用程序,index.php
                
<?php
    include_once( 'pizza.class.php' );
    
    $large = new Pizza( 'L', null );
    echo $large->price() . '<br />';
    
    $toppings = array();
    $toppings[] = 'pepperoni';
    $toppings[] = 'sausage';
    $xl = new Pizza( 'XL', $toppings );
    echo $xl->price() . '<br />';
    
    $special = new Pizza( 'XL' );
    $special->add( null );
    $special->add( 'pepperoni' );   
    $special->add( 'pepperoni' );   
    $special->add( 'anchovies' );   
    $special->add( 'anchovies' );
    $special->add( 'olives' );
    echo $special->price() . '<br />';
?>


清单 3. Pizza 类
                
<?php
    class Pizza {
        var $size;
        var $toppings = array();
        var $price;
        
        function Pizza( $size = "R", $toppings = null ) {
            $this->size = $size;
            $this->toppings = $toppings; 
        }
        
        function size() {
            return( $this->size );
        }
        
        function price() {
            $this->price = 10;
            $multiplier = 1;
            
            switch ( $this->size ) {
                case 'L': 
                    $multiplier = 1.25;
                    break;
                case 'XL':
                    $multiplier = 2;
                    break;
            }
            
            $this->price = $this->price + 
                ( sizeof( $this->toppings() ) ) * $multipler;
                
            return( $this->price );
        }
        
        function toppings() {
            return( $this->toppings );
        }
        
        function add( $topping ) {
            $this->toppings[] = $topping;
        }
    }
?>

按照以下步骤创建项目:

  1. 打开 Zend Studio 并选择 Project > New Project 来创建一个新项目。
  2. 将这个项目命名为 pizza(见图 8),然后单击 Next

    图 8. 创建新项目,pizza
    创建新项目,pizza

    在出现的窗口中,可以将一个或多个源代码目录添加到项目中,从而便于在 IDE 中寻找、打开、编辑和保存项目的所有代码。因为这是一个新项目,所以只添加一个新的源代码目录。

  3. 单击 Add path,找到您的个人站点目录,然后单击 New Folder(右上角第三个按钮)。
  4. 输入名称 pizza,单击这个条目(见图 9),然后单击 Add

    图 9. 创建新的源代码文件夹
    创建新的源代码文件夹

    向导窗口现在像图 10 这样。

    图 10. 项目现在有一个源代码文件夹
    项目现在有一个源代码文件夹

  5. 单击 Next

    下一组设置指定如何连接 Web(调试)服务器。

  6. 取消 Use System Defaults 复选框并输入应用程序的 URL,比如 localhost/~username/pizza/index.php(如果使用 Mac OS X)或 localhost/pizza/index.php(如果使用 Linux® 机器作为服务器,使用 Mac OS X 运行 Zend Studio),见图 11。

    图 11. 设置项目的 URL
    设置项目的 URL

  7. 单击 Finish

    Project 窗口中现在应该有一个名为 pizza 的文件夹。

  8. 选择 File > New File 两次。
  9. 复制清单 2 并将文本保存在文件系统的 pizza 文件夹中的 index.php 文件中。
  10. 复制清单 3 并将文本保存为 pizza 文件夹中的 Pizza.class.php 文件。
  11. 在 Project 面板中展开这个文件夹,就可以看到这两个 PHP 文件。

    图 12. 展开的项目文件夹
    展开的项目文件夹

我的代码出了什么问题?

现在可以调试这个应用程序了。单击 Play:应用程序运行,它的输出出现在 Debug Output 面板中。输出如下:

Content-type: text/html
10<br />10<br />10<br />

为什么这三个比萨饼的价格都是 $10?如果看一下 Debug Messages 面板,答案就清楚了:变量 $multipler 是未定义的,因为它是一个拼写错误,应该是 $multiplier

如果这个 bug 的原因不这么明显的话(常常是这种情况),还可以用另一种方法捕捉这个错误。双击 Pizza.class.php,将游标放在第 29 行上并选择 Debug > Add/Remove Breakpoint。这一行会变成红色的。双击 index.php 并单击 Play。程序在第 29 行停止执行。看一下 Debug 窗口,$this->price 到目前为止是正确的,$multiplier 也是正确的,那么发生了什么?单击向下箭头进行分步执行,会看到价格没有变化。哦!有一个拼写错误。纠正它,重新运行应用程序,每个比萨饼的价格应该不一样了。

作为一个练习,使用 IDE 分步探察另一个 bug,这是一个导致重复计价的逻辑错误。取消所有断点,然后在 index.php 中的第 14 行设置一个断点。在 toppings()add() 函数上设置断点,从而观察 Pizza 对象发生的情况。图 13 显示在计算价格期间这个对象的情况。


图 13. 错误的 Pizza 对象
错误的 Pizza 对象

为了纠正这个 bug,不允许 null 作为有效的配料(这实际上是 index.php 的错误,但是可以在 add() 函数中增加保护措施)并删除重复的配料。下面的代码可以解决这些问题。

        function toppings() {
            if ( is_array( $this->toppings ) ) {
                return( array_unique( $this->toppings ) );
            }
            
            return( 0 );
        }
        
        function add( $topping ) {
            if ( ! is_null( $topping ) ) {
                $this->toppings[] = $topping;
            }
        }

连接浏览器

最后一个练习是在浏览器中启动应用程序并在 IDE 中调用它。典型的情况是:观察浏览器中的输出,与 Web 页面进行交互,然后分步执行应用程序,观察它如何做出响应:

  1. 选择 Debug > Debug URL

    默认的设置可能就够了。如果默认设置不合适,那么将 Open Browser At 字段设置为 index.php 文件的 URL。对于这个示例,因为所有软件都在 Mac 上运行,所以选择 Local files, if available

  2. 单击 OK

    浏览器打开 Open Browser At 指定的 URL(包括许多为这个调试会话配置调试器的参数)。浏览器最初是空的,因为 IDE 很早就暂停了应用程序的执行。

  3. 切换到 IDE,将游标放在 index.php 的第一行,就在包含类文件的地方。然后可以分步执行这个应用程序。

如果应用程序包含表单,那么可以在表单处理函数上设置断点并查看输入的参数。在 Debug 窗口中可以查看所有全局环境变量、PHP 全局变量和每个 Web 请求的参数。

使用 Debug URL 窗口控制应用程序何时暂停。PHP Web 应用程序通常集中在一个目录中并包含一个主页(一般名为 index.php)。主页上的链接指向其他 PHP 页面,那些页面分别提供不同的特性。例如,URL .../store/index.php 可能是一个在线商店的主页;URL .../store/cart/index.php 可能实现购物车。

可以用 Zend Studio 调试某个应用程序根目录中的一个、几个或所有 PHP 页面。例如,如果购物车出现了错误,那么可以在打开 cart/ 中的任何页面时启动调试会话。也可以在浏览主页 index.php 时启动调试会话,调试整个应用程序。

更好的是,可以组合使用 Zend Studio、Internet Explorer® 或 Mozilla Firefox 以及 Zend Debugger Browser Toolbar,从而在浏览器中直接启动调试会话。可以调试当前页面、站点上的所有页面或某个表单的操作。





回页首


一种新的工作方式

交互式调试器就像是矿工头上的矿灯。诊断问题所需的艰苦劳动突然变得容易了,我们不必在黑暗中苦苦摸索了 —— 只需设置断点,分步执行代码,观察每个语句发生的情况。用一个笔记本记下每次暂停时的情况,观察并推测原因。

Zend Studio 不是调试器的惟一选择。试试其他调试器,寻找最适合您的工作习惯的调试器。无论如何,务必选用一种调试器。您会发现编写代码时离不开它。

 
Logo

更多推荐