Найдите ошибки в PHP-приложениях при помощи Xdebug (исходники)

Мартин Стрейчер

Хотя PHP можно использовать для создания сценариев командной строки для таких задач как системное администрирование и традиционная обработка данных, мощь языка особенно проявляется в Web-приложениях. В данной статье каждое PHP-приложение размещается на сервере и активизируется через прокси, такой как Apache, для обработки входящих запросов. Для каждого запроса за короткое время выполняется обычное PHP Web-приложение, выдающее Web-страницу или структуру XML-данных.

Учитывая кратковременность выполнения Web-приложений и их уровневую конструкцию (клиентское приложение, сеть, HTP-сервер, прикладной код и применяемая база данных), отловить баги (bugs) в PHP-коде может быть непросто. Даже если предположить, что все уровни, за исключением PHP-кода, работают безупречно, трассировка до обнаружения ошибки в PHP-коде может быть трудной, особенно (с некоторой иронией) если приложение использует все больше и больше классов.

PHP-выражение echo и функции var_dump(), debug_zval_dump() и print_r() являются обычными и очень популярными средствами отладки, помогающими решить различные проблемы. Однако как средства расследования эти выражения (и даже более надежный инструментарий, например, пакет PEAR Log) предоставляют доказательства, которые вы должны анализировать априори и вне контекста.

Кроме того, отладка посредством дедукции является подходом с позиции "грубой силы". Вы собираете данные и просеиваете их, пытаясь понять, что произошло. При отсутствии необходимой информации вы должны переделать ваш код, повторить предыдущие действия и начать исследование заново. Намного более эффективная стратегия - испытывать приложение во время его работы. Можно каталогизировать параметры запроса, просмотреть стек вызовов процедур, узнать значение любой переменной или объекта. Можно временно прервать выполнение приложения и получить уведомление об изменениях значения переменной. В некоторых случаях можно фактически интерактивно изменять переменные, задавая вопрос "Что если?".

Такое "живое" или интерактивное исследование обеспечивается специальным приложением, называемым отладчиком . Отладчик запускает или подключается к процессу для управления им и исследования его памяти. Либо, в случае с интерпретируемыми языками, отладчик может непосредственно интерпретировать код. Типичный современный отладчик может индексировать и просматривать исходный код, отображать сложные структуры данных в читабельном виде и одновременно отображать состояние программы, стек вызовов, выводимые программой данные и значения всех переменных. Например, для отладчика обычным является каталогизация и отображение свойств и методов класса.

В этой и следующей статьях я познакомлю вас с инструментальными средствами, которые непременно упростят PHP-отладку. В следующий раз я сконцентрируюсь на интерактивной отладке и программе Zend Debugger (надежном отладчике, специально разработанном для PHP), а также рассмотрю множество функций, которые он предлагает (Zend Debugger - это коммерческий продукт, предоставляемый как часть Zend PHP Integrated Development Environment (IDE)). Также я рассмотрю PHP-отладчик с открытым исходным кодом, если вы предпочитаете тратить деньги на пиво, а не на программы. В данной статье я сконцентрируюсь на сборе более качественных доказательств.

Как в сериале C.S.I., но применительно к компьютерам

Когда код становится неправильным, не давая ожидаемого результата или не работая вовсе, вы хотели бы получить ответ на четыре вопроса: где, что, почему и когда.

  • "Где" - номер строки (и в каком она файле), после которой выполнение приложения прекратилось.
  • "Что" привело код в негодность - подозреваемый, так сказать.
  • "Почему" - в чем природа ошибки. Возможно, это логическая ошибка, ошибка, вызванная взаимодействием с операционной системой, или то и другое.
  • "Когда" - контекст, в котором возникла ошибка. Что случилось непосредственно перед гибелью программы? Как и в любом преступлении, улики могут изобличить преступника, если собрать достаточное их количество.

Средство для проведения расследования Xdebug (использовалось в предыдущей статье для профилирования производительности PHP-приложения), как следует из его названия, предоставляет несколько функциональных возможностей для отображения состояния программы и является ценным исследовательским инструментом, который должен быть в вашем наборе. Будучи установленным, Xdebug вмешивается в процесс для предотвращения бесконечных рекурсий, добавляет в сообщения об ошибках информацию о трассировке стека и функций, следит за распределением памяти, а также выполняет некоторые другие функции. Xdebug содержит также набор функций, которые вы можете добавить в свой код для получения диагностических данных времени выполнения.

Например, приведенный ниже код использует горстку процедур xdebug_...() для оснащения функции callee() возможностью вывода точного месторасположения вызывающей функции, включая имя файла, номер строки и имя функции.

Листинг 1. Процедуры для оснащения функции callee() новыми возможностями

                
<?php
    function callee( $a ) {
        echo sprintf("callee() called @ %s: %s from %s",
            xdebug_call_file(),
            xdebug_call_line(),
            xdebug_call_function()
        );
    }

    $result = callee( "arg" );
?>

Этот код выводит:

callee() called @ /var/www/catalog/xd.php: 10 from {main}

Компоновка и установка Xdebug

Xdebug без труда компонуется из исходных кодов в UNIX-подобных операционных системах, в том числе Mac OS X. Если вы используете PHP в Microsoft Windows, можете загрузить модуль Xdebug в бинарном виде для последних версий PHP с Web-сайта Xdebug.

Давайте выполним компоновку и установим Xdebug для Debian "Sarge" Linux и PHP V4.3.10-19. На момент написания данной статьи новейшей версией Xdebug является V2.0.0RC4, выпущенная 17 мая 2007 года. Для работы вы должны иметь служебные программы phpize и php-config, а также возможность изменять системный конфигурационный файл php.ini (если этих программ у вас нет, обратитесь на PHP.net за исходными кодами и инструкциями по компоновке PHP с нуля). Выполните следующие действия:

  1. Загрузите Xdebug tarball (сжатый gzip .tar-архив). Это очень сделать просто при помощи команды wget:
     $ wget http://www.xdebug.org/files/xdebug-2.0.0RC4.tgz
                        

  2. Разархивируйте tarball и перейдите в каталог с исходным кодом:
    $ tar xzf xdebug-2.0.0RC4.tgz
    $ cd xdebug-2.0.0RC4
                        

  3. Запустите phpize, чтобы подготовить код Xdebug для вашей версии PHP:
    $ phpize
    Configuring for:
    PHP Api Version:         20020918
    Zend Module Api No:      20020429
    Zend Extension Api No:   20021010
    

    Результатом работы phpize является сценарий (очень к месту названный configure), используемый для настройки остального процесса компоновки.
  4. Выполните сценарий настройки:
    $ ./configure
    checking build system type... i686-pc-linux-gnu
    checking host system type... i686-pc-linux-gnu
    checking for gcc... gcc
    checking for C compiler default output file name... a.out
    checking whether the C compiler works... yes
    checking whether we are cross compiling... no
    checking for suffix of executables... 
    checking for suffix of object files... o
    ...
    checking whether stripping libraries is possible... yes
    appending configuration tag "F77" to libtool
    configure: creating ./config.status
    config.status: creating config.h
    

  5. Выполните компоновку расширения Xdebug, запустив make:
    $ make
    /bin/sh /home/strike/tmp/xdebug-2.0.0RC4/libtool
    --mode=compile gcc  -I.
    -I/home/strike/tmp/xdebug-2.0.0RC4 -DPHP_ATOM_INC
    -I/home/strike/tmp/xdebug-2.0.0RC4/include
    -I/home/strike/tmp/xdebug-2.0.0RC4/main
    -I/home/strike/tmp/xdebug-2.0.0RC4
    -I/usr/include/php4 -I/usr/include/php4/main
    -I/usr/include/php4/Zend -I/usr/include/php4/TSRM 
    -DHAVE_CONFIG_H  -g -O0 -c
    /home/strike/tmp/xdebug-2.0.0RC4/xdebug.c -o
    xdebug.lo mkdir .libs
    ...
    
    Build complete.
    (It is safe to ignore warnings about tempnam and tmpnam).
    

    Результатом работы make является расширение Xdebug, xdebug.so.
  6. Установите расширение:
    $ sudo make install
    Installing shared extensions:     /usr/lib/php4/20020429/
    

    Перед продолжением работы выделите и скопируйте каталог, который отобразила последняя команда. Этот путь очень важен для конфигурирования расширения на последнем шаге.
  7. Откройте файл php.ini в любимом текстовом редакторе и добавьте следующие строки:
    zend_extension = /usr/lib/php4/20020429/xdebug.so
    xdebug.profiler_enable = Off
    xdebug.default_enable = On
    

    Первая строка загружает расширение Xdebug; вторая запрещает функциональность профайлера в Xdebug (для упрощения), а третья разрешает функциональные возможности отладки.

Для проверки корректности установки и разрешения работы расширению Xdebug перезапустите ваш Web-сервер, затем создайте простое однострочное PHP-приложение с кодом <?php phpinfo(); ?>. Если вы укажете адрес файла в браузере (например, http://localhost/phpinfo.php) и прокрутите выведенную информацию вниз, то увидите что-то похожее на рисунок 1.

Рисунок 1. Проверка корректности установки и работы расширения Xdebug
Рисунок 1. Проверка корректности установки и работы расширения Xdebug

Примечание: Если раздела Xdebug нет в phpinfo(), Xdebug не загрузился. log-файлы ошибок вашего сервера Apache могут указать причину. К обычным ошибкам относится неправильный путь для zend_extension или конфликт с другим расширением. Например, если вы хотите использовать XCache и Xdebug, первым загружайте XCache. Однако, поскольку Xdebug предназначен для использования во время разработки и предполагая, что путь к xdebug.so указан правильно, запретите другие расширения и попробуйте еще раз. Затем можно повторно разрешить расширения для выполнения других проверок, например, эффективности кэширования. На сайте Xdebug приведены также некоторые другие советы по решению возможных проблем.

Конфигурирование Xdebug

Директивы (в самом левом столбце большой таблицы на рисунке 1) - это только некоторые из параметров, которые можно установить для изменения поведения расширения Xdebug. Все директивы указываются в файле php.ini. Некоторые из них конфигурируют средства отладки, другие настраивают работу профайлера. Игнорируя последние, давайте настроим Xdebug так, чтобы он помогал отлаживать PHP-код.

Ограниченная рекурсия

При использовании рекурсии (например, для вычисления чисел Фибоначчи) и некорректном указании условий завершения приложение может выполняться очень долго до тех пор, пока не использует все выделенное время или память. Для ограничения глубины рекурсии можно настроить параметр xdebug.max_nesting_level. Например, xdebug.max_nesting_level = 50 ограничивает рекурсию глубиной в 50 вложенных вызовов до принудительного завершения приложения. Для демонстрации выполните следующий код с разрешением работы Xdebug.

Листинг 2. Ограничение рекурсии

                
<?php
    function deep_end( ) {
        deep_end();
    }
    
    deep_end();
?>

Функция deep_end() совершенно буквально ведет в глубокий тупик. Xdebug вмешивается в процесс после 49 вызовов функции и выдает информацию, приведенную на рисунке 2 (кстати говоря, начальная активизация main() для запуска программы считается первым фреймом).

Рисунок 2. Xdebug завершает выполнение, если стек вызовов превышает его граничное значение
Рисунок 2. Xdebug завершает выполнение, если стек вызовов превышает его граничное значение

Если приложение использует рекурсию интенсивно для решения больших проблем методом "разделяй и властвуй", соответственно установите значение этого параметра "поглубже". В противном случае установите xdebug.max_nesting_level в номинальное значение для более быстрого прекращения вышедших из под контроля последовательностей вызовов функций.

Ответы на четыре простых вопроса

При возникновении ошибок нужно ответить на четыре вопроса. Xdebug может предоставить всю нужную информацию, причем немедленно. Вот для начала несколько полезных настроек; их можно настроить более точно в любое время.

Листинг 3. Ошибки

                
xdebug.dump_once = On
xdebug.dump_globals = On
xdebug.dump_undefined = On
xdebug.dump.SERVER = REQUEST_METHOD,REQUEST_URI,HTTP_USER_AGENT
xdebug.dump.REQUEST=*

xdebug.show_exception_trace = On
xdebug.show_local_vars = 1
xdebug.var_display_max_depth = 6

Настройки xdebug.dump_once, xdebug.dump_globals, xdebug.dump_undefined и xdebug.dump_SUPERGLOBAL (где SUPERGLOBAL может быть COOKIE, FILES, GET, POST, REQUEST, SERVER или SESSION) управляют тем, какие суперглобальные переменные PHP включаются во все диагностические результаты.

Установите 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']. Если вы хотите вывести все значения массива superglobal, используйте символ звездочки (*), например, xdebug.dump_REQUEST=*. Если вы установите xdebug.dump_undefined в значение On и не установите именованную переменную superglobal, она все равно выводится со значением undefined.

Строка xdebug.show_exception_trace = On разрешает трассировку исключительных ситуаций, даже если вы перехватили исключительную ситуацию. Строка xdebug.show_local_vars = 1 выводит все локальные переменные самой внешней области видимости каждого вызова функции, включая еще не инициализированные переменные. А xdebug.var_display_max_depth = 6 указывает глубину вывода комплексной переменной.

Объединение всех настроек

В листинге 4 показаны все важные настройки для Xdebug в файле php.ini.

Листинг 4. Настройки для файла php.ini

                
zend_extension = /usr/lib/php4/20020429/xdebug.so
xdebug.default_enable = On
xdebug.show_exception_trace = On
xdebug.show_local_vars = 1
xdebug.max_nesting_level = 50
xdebug.var_display_max_depth = 6

xdebug.dump_once = On
xdebug.dump_globals = On
xdebug.dump_undefined = On
xdebug.dump.REQUEST = *
xdebug.dump.SERVER = REQUEST_METHOD,REQUEST_URI,HTTP_USER_AGENT

Сохраните эти настройки в файле php.ini, а затем перезапустите ваш Web-сервер.

Интерпретация данных дампа

В следующем примере продемонстрировано, что происходит при возникновении ошибки. Измените код "off-the-deep-end" для соответствия листингу 5.

Листинг 5. Измененный ошибочный код

                
<?php
    function deep_end( $count ) {
        // добавить 1 к фрейму count
        $count += 1;

        if ( $count < 48 ) {
                deep_end( $count );
        }
        else {
                trigger_error( "going off the deep end!" );
        }
    }

    // Вызывается main(), чтобы начать выполнение программы,  
    // то есть стек вызовов начинается с одного фрейма
    deep_end( 1 );
?>

При выполнении этого нового кода, вы должны увидеть значительно больше информации, как показано ниже.

Рисунок 3. Дамп суперглобальных, локальных переменных и переменных при ошибке
Рисунок 3. Дамп суперглобальных, локальных переменных и переменных при ошибке

Текст передаваемого в trigger_error сообщения показан вверху. Внизу расположен список запрошенных элементов $_SERVER и список определенных элементов $_REQUEST. В самом внизу находится список переменных области видимости #48, что является вызовом deep_end(), согласно манифесту. В этом вызове переменная $count имела значение integer 48. С такой конфигурацией Xdebug вы теперь имеете больше улик для поиска преступника.

Вот еще один совет: Xdebug предоставляет расширенную функцию var_dump(), которая особенно полезна для массивов и классов PHP. Например, в листинге 6 приведен простой (PHP V4) класс и экземпляры.

Листинг 6. Класс и экземпляры PHP V4

                
<?php
    class Person {
        var $name;
        var $surname;
        var $age;
        var $children = array();

        function Person( $name, $surname, $age, $children = null) {
            $this->name = $name;
            $this->surname = $surname;
            $this->age = $age;
            foreach ( $children as $child ) {
                $this->children[] = $child;
            }
        }
    }   

    $boy = new Person( 'Joe', 'Smith', 4 );
    $girl = new Person( 'Jane', 'Smith', 6 );
    $mom = new Person( 'Mary', 'Smith', 34, array( $boy, $girl ) );

    var_dump( $boy, $mom );
?>

А в листинге 7 показана информация, выводимая функцией var_dump().

Листинг 7. Вывод var_dump()

                
object(person)
  var 'name' => string 'Joe' (length=3)
  var 'surname' => string 'Smith' (length=5)
  var 'age' => int 4
  var 'children' => 
    array
      empty
      
object(person)
  var 'name' => string 'Mary' (length=4)
  var 'surname' => string 'Smith' (length=5)
  var 'age' => int 34
  var 'children' => 
    array
      0 => 
        object(person)
          var 'name' => string 'Joe' (length=3)
          var 'surname' => string 'Smith' (length=5)
          var 'age' => int 4
          var 'children' => 
            array
              empty
      1 => 
        object(person)
          var 'name' => string 'Jane' (length=4)
          var 'surname' => string 'Smith' (length=5)
          var 'age' => int 6
          var 'children' => 
            array
              empty

При использовании Xdebug с классами PHP V5 в дамп входят такие атрибуты как public, private и protected.

Трассировка кода

Исправление ошибки в коде (также как и раскрытие убийства) часто требует воссоздания подробной временной шкалы. Например, утечка памяти обычно не проявляет себя как ошибочная ситуация. Напротив, операции выполняются нормально до тех пор, пока не закончится память и приложение внезапно не завершится. Если утечка памяти происходит из-за конкретных запросов, ошибки могут проявляться не всегда, и их будет сложно прогнозировать. Отображение использования памяти во времени показало бы серьезность утечки. Подробная временная шкала (например, от функции к функции) могла бы указать на источник утечки.

Xdebug может обеспечить вывод подробной временной шкалы как трассировки выполнения . При разрешенной трассировке Xdebug регистрирует каждый вызов функций, включая аргументы функции и возвращаемое значение. Можно отформатировать каждый log-файл для более удобного его восприятия человеком или машиной. Первый вариант вы можете просматривать сами, а для второго можно написать отдельное приложение для анализа.

Так же как и для дампов, Xdebug имеет несколько параметров для настройки трассировки в php.ini. Следующие настройки, например, обеспечивают вывод наиболее подробной информации:

Листинг 8. Настройка трассировки

                
xdebug.trace_format = 0
xdebug.auto_trace = On
xdebug.trace_output_dir = /tmp/traces
xdebug.trace_output_name = trace.%c.%p

xdebug.collect_params = 4
xdebug.collect_includes = On
xdebug.collect_return = On
xdebug.show_mem_delta = On

Настройка xdebug.auto_trace = 1 автоматически разрешает трассировку до выполнения любого PHP-сценария. В качестве альтернативы можно установить xdebug.auto_trace = 0 и использовать функции xdebug_start_trace() и xdebug_stop_trace() из вашего кода для разрешения и запрета трассировки соответственно. Однако если xdebug.auto_trace установлен в значение 1, можно начать трассировку до включения сконфигурированного auto_prepend_file.

Параметры xdebug.trace_ouput_dir и xdebug.trace_output_name управляют тем, где сохраняется информация трассировки. В данном примере все файлы сохраняются в /tmp/traces, и каждый файл начинается с trace, за которым следует имя PHP-сценария (%s) и идентификатор процесса (%p). Названия всех файлов трассировки Xdebug заканчиваются суффиксом .xt.

По умолчанию Xdebug отображает поля времени, использования памяти, имени функции и глубины вызова функции. Если установить xdebug.trace_format в значение 0, информация выводится в виде, удобном для чтения человеком (для машинного формата используется значение 1). Кроме того, можно обнаружить рост или уменьшение использования памяти при указании xdebug.show_mem_delta = 1, а тип и значения входящих параметров можно выводить, указав xdebug.collect_params = 4. Для отслеживания значения, возвращаемого каждой функцией, установите xdebug.collect_return = 1.

Пришло время другого примера. Создайте каталог /tmp/traces, измените его атрибуты на world-readable и world-writable при помощи mkdir /tmp/traces; chmod a+rwx /tmp/traces (если вы сомневаетесь в том, нужно ли делать этот каталог доступным для всех, по крайней мере, сделайте его доступным для пользователя Web-сервера - обычно www или nobody ). Добавьте в файл php.ini указанные выше настройки, перезапустите Web-сервер и снова укажите в адресной строке браузера приложение phpinfo(). Информация трассировки должна выглядеть примерно так, как показано в листинге 9.

Листинг 9. Полная трассировка

                
TRACE START [2007-06-06 14:04:55]
    0.0003       9440    +9440   -> {main}() /var/www/catalog/t/info.php:0
    0.0005       9440       +0     -> phpinfo() /var/www/catalog/t/info.php:1
                                   >=-> TRUE
                                 >=-> 1
    0.2351       9208
TRACE END   [2007-06-06 14:04:55]

Здесь main() вызывает phpinfo(), которая возвращает TRUE. Когда завершается main(), она возвращает 1. Затем, укажите в адресной строке PHP-приложение "deep end" или какое-то другое для генерирования более реальной трассировки.

В листинге 10 показана трассировка PHP-генератора чисел Фибоначчи из предыдущей статьи, вычисляющего четыре числа Фибоначчи:

Листинг 10. Трассировка PHP-генератора чисел Фибоначчи

                
TRACE START [2007-06-06 14:17:17]
    0.0004      16432   +16432   -> {main}() /var/www/catalog/t/fibonacci.php:0
    0.0006      16696     +264     -> fib('4') /var/www/catalog/t/fibonacci.php:35
    0.0007      16696       +0       -> fib(3) /var/www/catalog/t/fibonacci.php:7
    0.0007      16736      +40         -> fib(2) /var/www/catalog/t/fibonacci.php:7
    0.0007      16848     +112           -> fib(1) /var/www/catalog/t/fibonacci.php:7
                                         >=> 1
    0.0008      16904      +56           -> fib(0) /var/www/catalog/t/fibonacci.php:7
                                         >=> 0
                                       >=> 1
    0.0009      16904       +0         -> fib(1) /var/www/catalog/t/fibonacci.php:7
                                       >=> 1
                                     >=> 2
    0.0009      16904       +0       -> fib(2) /var/www/catalog/t/fibonacci.php:7
    0.0009      16904       +0         -> fib(1) /var/www/catalog/t/fibonacci.php:7
                                       >=> 1
    0.0010      16904       +0         -> fib(0) /var/www/catalog/t/fibonacci.php:7
                                       >=> 0
                                     >=> 1
                                   >=> 3
                                 >=> 1
    0.0011      12528
TRACE END   [2007-06-06 14:17:17]

В первом столбце показано время, во втором - суммарное использование памяти, в третьем - инкрементное использование памяти, в четвертом - вызовы функций, включая параметры.

Строки, отмеченные символами >=>, показывают возвращаемое значение из каждой функции (для сопоставления вызова функции с возвращаемым ею значением нужно найти -> с соответствующим отступом). Опять же, последнее значение >=> 1 является значением, возвращаемым функцией main().

Для vim его автор Xdebug Дэрик Ретанс (Derick Rethans) предоставляет набор правил цветового выделения синтаксиса для файлов трассировки Xdebug. Эти правила содержатся в файле xt.vim в пакете исходных кодов Xdebug. В современных дистрибутивах Linux просто скопируйте xt.vim в $VIMRUNTIME/syntax/xt.vim, а затем запустите vim tracefile .xt. На рисунке 4 показана трассировка генератора чисел Фибоначчи с цветовым выделением в vim.

Рисунок 4. Синтаксический файл vim для файлов трассировки Xdebug облегчает анализ
Рисунок 4. Синтаксический файл vim для файлов трассировки Xdebug облегчает анализ

Надоедливые PHP-паразиты в опасности

Исправление ошибок в PHP-коде может быть не простой задачей. Но если у вас есть система разработки, и вы можете установить Xdebug, уничтожить эти баги становится намного проще. Xdebug может показать трассировку стека, вывести дамп даже комплексных переменных, проследить использование памяти во времени и выполнить эффективное посмертное вскрытие при возникновении ошибки или аварии (не если , а когда ).


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=17921