Статьи

Вклад в PHP: как исправлять ошибки в ядре PHP

Ранее мы рассмотрели возможность внесения вклада в документацию PHP. Теперь мы рассмотрим, как подключиться к ядру PHP. Для этого мы рассмотрим рабочий процесс исправления простой ошибки в ядре.

PHP логотип

Поскольку отправка новых функций в PHP уже была объяснена довольно хорошо , мы не будем здесь это рассматривать. Кроме того, эта статья не стремится преподавать внутренности PHP. Для получения дополнительной информации, пожалуйста, смотрите мои предыдущие посты о добавлении функций в PHP .

Устранение ошибок

Исправление ошибок в ядре — отличный способ познакомиться с внутренностями PHP. Это требует только базовых знаний C и является простым способом помочь с усилиями по улучшению PHP. Но прежде чем сделать это, необходимо ознакомиться с управлением версиями PHP.

Жизненный цикл управления версиями PHP

Минорные версии PHP следуют годовому циклу выпуска, и каждая минорная версия имеет 3 года поддержки. Первые 2 года предоставляют «активную поддержку» для общих исправлений ошибок, а последний год — «поддержку безопасности» только для исправлений безопасности. После окончания трехлетнего цикла поддержка этой версии PHP прекращается.

В настоящее время поддерживаемые версии PHP можно увидеть на сайте php.net . На момент написания статьи PHP 5.5 поддерживал безопасность, а PHP 5.6 и 7 активно поддерживали.

Исправление ошибки

Чтобы продемонстрировать базовый рабочий процесс, давайте исправим ошибку # 71635 из bugs.php.net . В отчете об ошибке указано, что при вызове DatePeriod::getEndDate() когда конечная дата недоступна. Поэтому первое, что мы хотим сделать, это подтвердить его правильность.

Для ошибок, которые выглядят тривиально (практически без требований к настройке среды), мы можем начать с быстрого просмотра, можно ли воспроизвести ошибку в 3v4l . (3v4l — удобный инструмент, который запускает фрагмент кода на сотнях версий PHP.) Это позволяет нам увидеть все затронутые версии PHP, что удобно, чтобы быстро выяснить, затрагиваются ли старые, все еще поддерживаемые версии PHP. Как мы видим, PHP завершает работу с сегфоутом для всех версий с 5.6.5 по 7.0.4.

3v4l результат скриншот

Независимо от того, может ли ошибка быть воспроизведена в 3v4l или нет, нам нужно будет реплицировать ее локально, прежде чем мы сможем исправить ее. Для этого вам нужно будет форкнуть php / php-src и локально клонировать ваш форк. Если вы уже делали это некоторое время назад, вам может потребоваться обновить клон, а также получить все последние версии с тегами (с помощью git remote update ).

Мы собираемся поработать над веткой PHP 5.6, так как это самая низкая версия PHP, затронутая этой ошибкой (пока еще активно поддерживаемая). (Если бы эта ошибка повлияла на PHP 5.5, мы все равно проигнорировали бы эту версию и работали бы против PHP 5.6, поскольку эта ошибка не связана с безопасностью.) Стандартный рабочий процесс для отправки исправлений ошибок состоит в том, чтобы нацелить исправление на самое уязвимое место (хотя все еще поддерживается) Версия PHP. Затем один из разработчиков php / php-src объединит исправление вверх при необходимости.

Итак, давайте извлечем копию ветки PHP 5.6 для работы в:

 git checkout - b fix - dateperiod - segfault upstream / php - 5.6 

Затем мы собираем PHP и пытаемся воспроизвести segfault локально, создав файл (скажем, segfault.php ) со следующим кодом:

 <?php $period = new DatePeriod ( new DateTimeImmutable ( "now" ) , new DateInterval ( "P2Y4DT6H8M" ) , 2 ) ; var_dump ( $period - > getEndDate ( ) ) ; 

Затем мы запускаем segfault.php с только что созданным двоичным файлом PHP:

 sapi / cli / php - n segfault . php 

(Флаг -n означает, что файл php.ini не будет использоваться для конфигурации. Это особенно удобно для использования, если в ваш файл php.ini по умолчанию загружены пользовательские расширения, поскольку это предотвратит появление ошибок при загрузке. каждый раз, когда вы выполняете файл с локальным двоичным файлом PHP.)

После подтверждения того, что мы можем запустить это локально, мы можем создать тест для него. Давайте назовем этот тестовый файл bug71635.phpt и поместим его в папку ext / date / tests / со следующим содержимым:

 -- TEST -- Bug #71635 (segfault in DatePeriod::getEndDate() when no end date has been set) -- FILE -- <?php date_default_timezone_set ( 'UTC' ) ; $period = new DatePeriod ( new DateTimeImmutable ( "now" ) , new DateInterval ( "P2Y4DT6H8M" ) , 2 ) ; var_dump ( $period - > getEndDate ( ) ) ; ?> -- EXPECT -- NULL 

Запуск этого одиночного теста показывает, что он не проходит:

 make test TESTS = ext / date / tests / bug71635 . phpt 

Теперь мы запускаем отладчик по нашему выбору в файле segfault.php, который мы создали ранее. (Я использую LLDB, потому что это то, с чем Mac OS X сейчас идет в комплекте, но GDB — еще один подобный отладчик, у которого есть перекрывающиеся команды .)

 lldb sapi / cli / php a . php 

(Команда -n на этот раз не использовалась, так как, похоже, она работает с lldb.)

Теперь мы в отладчике LLDB, набираем команду run для run файла. Он должен показать, где в коде произошла ошибка:

Ошибка отображения

Хотя первый кадр, кажется, не показывает нам ничего чрезмерно значимого (если вы не программируете в asm), мы можем видеть, что программа остановилась из-за EXC_BAD_ACCESS . Он также показал нам, что адрес указателя, которым он пытался манипулировать, был 0x0 , поэтому мы можем видеть, что у нас есть нулевой доступ к указателю.

Использование команды bt показывает нам обратный след segfault (каждый кадр, ведущий к segfault). Глядя на кадр № 1 (вводя frame select 1 ), мы снова в коде C и видим строку, вызывающую проблему:

Обнаружена проблемная линия

Отсюда можно сделать вывод, что виновником является dpobj->end оценивается как dpobj->end , и, таким образом, попытка разыменования вызывает segfault. Итак, мы dpobj->end проверку выше этого, чтобы увидеть, является ли dpobj->end нулевым указателем, и если это так, просто вернитесь из функции (делая это как можно раньше):

 PHP_METHOD ( DatePeriod , getEndDate ) { php_period_obj * dpobj ; php_date_obj * dateobj ; if ( zend_parse_parameters_none ( ) == FAILURE ) { return ; } dpobj = ( php_period_obj * ) zend_object_store_get_object ( getThis ( ) TSRMLS_CC ) ; + + if ( ! dpobj - > end ) { + return ; + } php_date_instantiate ( dpobj - > start_ce , return_value TSRMLS_CC ) ; dateobj = ( php_date_obj * ) zend_object_store_get_object ( return_value TSRMLS_CC ) ; dateobj - > time = timelib_time_ctor ( ) ; * dateobj - > time = * dpobj - > end ; if ( dpobj - > end - > tz_abbr ) { dateobj - > time - > tz_abbr = strdup ( dpobj - > end - > tz_abbr ) ; } if ( dpobj - > end - > tz_info ) { dateobj - > time - > tz_info = dpobj - > end - > tz_info ; } } 

(При возврате из метода неявным образом функция возвращает null (как и все внутренние функции PHP при сбое). Это потому, что переменная return_value (которая доступна в любом определении функции) содержит фактическое возвращаемое значение функции и по умолчанию ноль. )

Итак, давайте создадим PHP и снова запустим наш тест:

 make test TESTS = ext / date / tests / bug71635 . phpt 

Теперь должно пройти! Теперь мы можем просто зафиксировать обновленный файл и соответствующий тест на наличие ошибок, а затем отправить PR в ветку 5.6 php / php-src .

Вывод

Эта статья продемонстрировала простой рабочий процесс, используемый при устранении ошибок в ядре. Устранение ошибок — отличная отправная точка для знакомства с внутренними компонентами PHP, и для этого требуется очень мало знаний о C.

Исправление ошибок также служит прекрасной серией небольших программных задач для тех, кому надоели алгоритмические проблемы, обнаруженные на Project Euler и подобных веб-сайтах. И с более чем 5000 открытых отчетов об ошибках , безусловно, нет недостатка в устранении ошибок!