Maxime Veber
Lead developer @ BiiG.
Vous avez écrit un bout de code qui fait totalement planter PHP : impossible d’avoir une stacktrace pour vous aider à savoir d’où cela vient. Vous êtes au bon endroit : nous allons voir comment obtenir de précieuses informations sur le bug que vous rencontrez. Cet article est orienté linux/ubuntu. Une approche docker-compose sera également proposée.
⚠️ Tout au long de cet article je parlerai de php en mode debug avec l’utilisation des symboles de debug. Si ce n’est pas le mode de fonctionnement de PHP par défaut, c’est en partie parce que l’exécution dans ce mode est beaucoup plus lente qu’à la normale. Pensez donc à rétablir un environnement de développement classique après vos tests. Je recommande même la compilation d’une nouvelle version de PHP en local pour vos essais (cf l’avant dernier chapitre).
Considérons le script PHP suivant qui comporte une erreur évidente :
// Fichier bug.php
$x = new AppendIterator;
$x->append($x);
Cette erreur génère l’output suivant :
Segmentation fault
Nous allons déboguer à l’aide de gdb. Pour cela vous devez :
$ sudo apt-get install gdb
Note: pour les plus téméraires, il existe un outil en python qui offre la possibilité d'utiliser GDB dans une interface web depuis quelques temps. Plus d’infos sur le github du projet : https://github.com/cs01/gdbgui
# Préférez la version responsable de votre version de PHP ! Mais ces scripts ne sont pas souvent mis à jour.
$ wget -O - https://raw.githubusercontent.com/php/php-src/master/.gdbinit > ~/.gdbinit
Le coredump est un fichier qui contient l’enregistrement d’une exécution de programme. Cela permet de voir l’état de la mémoire et l’avancement dans le programme au moment du bug facilement.
Tout d’abord et de manière générale, pour déboguer votre application vous aurez besoin au préalable d’effectuer quelques actions :
# Vérifiez la taille autorisée
$ ulimit -c
$ ulimit -a # liste exhaustive des ulimits avec leur signification
# Modifier la taille autorisée
$ ulimit -c unlimited # Vous pouvez aussi spécifier une très grande valeur, les coredump sont souvent lourds
# echo "/tmp/core-%t-%p" > /proc/sys/kernel/core_pattern
%p
représente le numéro du processus et
%t
est le timestamp, ceci évite d’écraser un ancien coredump.
Cette valeur est susceptible de changer suivant les paquets que vous installez, au cours du temps et au reboot. Pensez à la vérifier si le core dump n’est pas au bon endroit.
Sur une installation ubuntu standard, vous avez une version “php debug” disponible dans les packages. La solution la plus simple consiste à l’utiliser :
# 1. Ajout des dépôts de symboles
# https://wiki.ubuntu.com/Debug%20Symbol%20Packages
$ echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse" | \
sudo tee -a /etc/apt/sources.list.d/ddebs.list
$ sudo apt-get update
# 2. Installation des symboles de debug
$ sudo apt-get install php-cli-dbg php7.0-fpm-dbgsym
Pour les utilisateurs du ppa oerdnj/deb.sury.org, vous trouverez un moyen de vous en sortir sur le wiki https://github.com/oerdnj/deb.sury.org/wiki/Debugging-Symbols
À partir de là, vous êtes déjà capable de générer un coredump facilement :
$ php bug.php
Segmentation fault (core dumped)
L’output a changé et nous informe que des informations ont été enregistrées (« core dumped »). Il ne reste plus qu’à lire ce dump avec gdb :
$ ls /tmp
core-1512403995-27412
$ gdb php /tmp/core-1512403995-27412
Pour générer un coredump avec php-fpm, vous devrez au préalable configurer php-fpm.
Pour cela, éditez le fichier /etc/php/7.0/fpm/php-fpm.conf
.
Décommentez la ligne rlimit_core
et spécifiez la valeur « unlimited ».
; Set max core size rlimit for the master process.
; Possible Values: 'unlimited' or an integer greater or equal to 0
; Default Value: system defined value
rlimit_core = unlimited
Redémarrez ensuite fpm via sudo service php7.0-fpm restart
.
Lorsque vous exécuterez votre script, le dump sera alors effectué. Vous pouvez en trouver la preuve dans les logs de php-fpm :
# Fichier /var/log/php7.0-fpm.log
[03-Dec-2017 17:44:30] WARNING: [pool www] child 27719 exited on signal 11 (SIGSEGV - core dumped) after 5.293810 seconds from start
[03-Dec-2017 17:44:30] NOTICE: [pool www] child 27723 started
Vous devez ensuite trouver le binaire de php-fpm (je l’ai trouvé dans
/etc/init.d/php7.0-fpm
). Pour ubuntu je suppose que vous pouvez
vous attendre à quelque chose de similaire à /usr/sbin/php-fpm7.0
.
Il nous suffit à présent de lancer gdb en lui donnant l’exécutable en paramètre :
$ ls /tmp
core-1512323070-27719
$ gdb /usr/sbin/php-fpm7.0 /tmp/core-1512323070-27719
Une fois gdb lancé, vous pouvez tout de suite lire la backtrace en C en utilisant la commande bt. Mais nous, développeurs PHP, ce qui nous intéresse c’est plutôt quelque chose de lisible dans notre langage préféré. C’est là que nous allons utiliser le script .gdbinit récupéré préalablement sur le dépôt de PHP.
(gdb) source ~/.gdbinit
(gdb) zbacktrace
[0x7fd3627bce60] AppendIterator->rewind() [internal function]
[0x7fd3627bce00] AppendIterator->rewind() [internal function]
[0x7fd3627bcda0] AppendIterator->rewind() [internal function]
[0x7fd3627bcd40] AppendIterator->rewind() [internal function]
[0x7fd3627bcce0] AppendIterator->rewind() [internal function]
[0x7fd3627bcc80] AppendIterator->rewind() [internal function]
...
On peut alors utiliser des commandes définies dans le .gdbinit
. Vous pouvez utiliser
la commande zbacktrace pour afficher une backtrace en mode PHP.
Ici la boucle infinie générée en interne nous saute aux yeux : victoire, nous
savons d’où vient le bug !
Pour en savoir plus sur les commandes disponibles je vous encourage à lire le chapitre dédié.
Vous l’avez peut être remarqué, lorsqu’on lance gdb avec le coredump nous nous retrouvons avec une erreur similaire à celle-ci à chaque fois :
/build/php7.0-dFe1Vt/php7.0-7.0.22/Zend/zend_objects_API.c: No such file or directory.
Cette erreur est présente car gdb n’est pas capable de retrouver les fichiers source. Et pour cause : ils ne sont pas présents ! Pourtant vous pouvez les télécharger à tout moment. Utilisez la commande suivante dans le dossier de votre choix pour les télécharger :
# Cette commande ne s’utilise pas en root
$ apt-get source php7.0-fpm
Une fois les sources de votre package téléchargées, vous pouvez spécifier à gdb leur nouvelle localisation :
(gdb) set substitute-path /build/php7.0-dFe1Vt/php7.0-7.0.22/ /your/path/to/php7.0-7.0.22/
Vous pouvez à présent utiliser gdb pour vous situer dans le code de PHP au besoin :
(gdb) list
139 ZEND_OBJECTS_STORE_ADD_TO_FREE_LIST(handle);
140 }
141 /* }}} */
142
143 ZEND_API void zend_objects_store_del(zend_object *object) /* {{{ */
144 {
145 /* Make sure we hold a reference count during the destructor call
146 otherwise, when the destructor ends the storage might be freed
147 when the refcount reaches 0 a second time
148 */
Si vous utilisez un simple Dockerfile, je vous recommande de tenter les cas précédents. En revanche, si vous utilisez docker-compose vous vous sentirez quelques peu désemparés car l’utilisation de PHP avec les symboles n’est pas du tout prévue par l’image PHP « officielle ».
Nous devons donc tricher. L’astuce consiste à copier/coller le Dockerfile
correspondant à l’image que vous utilisez et remplacer le FROM php:7.0
par le contenu du fichier original. Pour moi il s’agit de PHP FPM 7.0 basé sur
debian jessie (si vous ne savez pas quoi prendre entre debian et alpine, prenez debian).
Pour moi il s’agit donc de ce Dockerfile. Vous trouverez la liste de tous les Dockerfile PHP à cette adresse :
https://github.com/docker-library/php
Après l’avoir copié, il vous faudra le modifier pour l’adapter à nos besoins :
Voici donc à quoi ressemble ma commande ./configure
dans le Dockerfile :
./configure \
--build="$gnuArch" \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \
\
--disable-cgi \
\
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
--enable-ftp \
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
--enable-mbstring \
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
--enable-mysqlnd \
\
--with-curl \
--with-libedit \
--with-openssl \
--with-zlib \
--enable-debug \ # <----- On a simplement rajouté ceci !
\
# bundled pcre is too old for s390x (which isn't exactly a good sign)
# /usr/src/php/ext/pcre/pcrelib/pcre_jit_compile.c:65:2: error: #error Unsupported architecture
--with-pcre-regex=/usr \
--with-libdir="lib/$debMultiarch" \
\
$PHP_EXTRA_CONFIGURE_ARGS
Le point important est la ligne --enable-debug
.
&& make install \
#&& { find /usr/local/bin /usr/local/sbin -type f -executable -exec strip --strip-all '{}' + || true; } \
&& make clean \
Une fois votre Dockerfile complet, vous devez re-build votre docker fpm.
Vous croyez que la galère allait s’arrêter là ? Raté. Il vous faut maintenant modifier deux valeurs du kernel… Auquel vous n’avez pas accès avec docker pour une question évidente de sécurité. Voici la marche à suivre pour tout de même réussir à modifier chacune des valeurs qui nous intéresse.
Pour modifier la valeur de ulimit -c
dans votre docker vous allez devoir modifier
votre configuration dans le fichier docker-compose.yml
:
version: '2'
services:
your-docker-name:
build: docker/php-fpm
ulimits:
core: 100000000000
Nous devons spécifier une très grande valeur car il n’est pas possible de spécifier « unlimited ». Vous devez redémarrer vos dockers pour que cela soit pris en compte.
Il nous faut maintenant réussir à modifier le core_pattern
localisé dans le
fichier /proc/sys/kernel/core_pattern
. Pour cela nous devons lancer un
docker en mode privilégié :
$ docker run -it --privileged alpine sh
# echo "/tmp/core-%t-%p" > /proc/sys/kernel/core_pattern
Cela modifie la valeur pour tous les dockers.
Tout est prêt. Exécutez votre page ou votre script.
Côté nginx vous recevrez une erreur « 502 Bad Gateway » et les logs de votre docker afficheront un message similaire à ceci :
your-docker-name_1 | [04-Dec-2017 18:53:54] WARNING: [pool www] child 6 exited on signal 11 (SIGSEGV - core dumped) after 393199.005907 seconds from start
your-docker-name_1 | [04-Dec-2017 18:53:54] NOTICE: [pool www] child 25 started
Installez gdb et exploitez le coredump comme décrit dans la section
présentant l’exploitation de coredump avec php-fpm. Notez que le binaire
est probablement situé dans /usr/local/sbin/php-fpm
dans votre container.
Cela peut paraître barbare mais cette méthode a plusieurs avantages :
Je vais détailler la compilation telle qu’elle se présente aujourd’hui, c’est à dire la compilation d’une pré-version de PHP 7.3. Dans le monde réel n’oubliez pas d’utiliser un tag pour récupérer la version qui vous convient.
Pour compiler PHP il faut tout d’abord aller télécharger les sources. Celles-ci sont disponibles sur Github :
$ git clone git@github.com:php/php-src.git && cd php-src
Vous aurez besoin d’installer tout un tas de librairies et surtout leur version « dev » des packages.
$ sudo apt-get update
$ sudo apt-get install build-essential \
autoconf \
re2c \
bison \
libxml2-dev \
libcurl4-openssl-dev \
libssl-dev \
libjpeg-dev \
libpng-dev \
libfreetype6-dev \
libpq-dev \
libxslt-dev \
libzip-dev
Vous pouvez à présent lancer la compilation :
$ buildconf --force
$ make clean
$ ./configure \
--with-pdo-pgsql \
--with-zlib-dir \
--with-freetype-dir \
--enable-mbstring \
--with-libxml-dir=/usr \
--enable-soap \
--enable-calendar \
--with-curl \
--with-zlib \
--with-gd \
--disable-rpath \
--enable-inline-optimization \
--with-zlib \
--enable-sockets \
--enable-sysvsem \
--enable-sysvshm \
--enable-pcntl \
--enable-mbregex \
--enable-exif \
--enable-bcmath \
--with-mhash \
--enable-zip \
--with-pcre-regex \
--with-pdo-mysql \
--with-mysqli \
--with-mysql-sock=/var/run/mysqld/mysqld.sock \
--with-jpeg-dir=/usr \
--with-png-dir=/usr \
--with-openssl \
--with-fpm-user=www-data \
--with-fpm-group=www-data \
--with-libdir=/lib/x86_64-linux-gnu \
--enable-ftp \
--with-kerberos \
--with-gettext \
--with-xmlrpc \
--with-xsl \
--enable-opcache \
--enable-intl \
--enable-fpm \
--enable-debug
$ make -j "$(nproc)"
Ceci est la configuration que j’ai choisie. Vous pouvez modifier beaucoup d’options,
notamment supprimer MySQL pour PostgreSQL. La seule option réellement importante
c’est l’option --enable-debug
.
Gardez en tête que l’idéal reste d’avoir une compilation qui soit la plus analogue
possible à votre environnement habituel, afin de pouvoir reproduire l’erreur avec un
maximum de similitudes entre la version PHP compilée à la main et celle fournie par
votre package manager.
ℹ️ Il est possible que certaines options n’existent pas ou que d’autres soient manquantes
pour vous, n’hésitez pas à consulter la liste des options de configure
http://php.net/manual/fr/configure.about.php
Vous pourriez bien entendu utiliser php-fpm avec nginx mais cela complexifie nos debugs avec gdb. Je préconise une utilisation de php plus simple :
$ /path/to/your/php/src/folder/sapi/cli/php -S localhost:8000 web/app.php # index.php si vous utilisez Sf4!
Cela vous permettra de rejouer facilement l’échec dans gdb.
Tout d’abord pour déboguer avec gdb et un peu de confort, vous devez lancer PHP via gdb :
$ gdb -args /path/to/your/php/src/folder/sapi/cli/php -args -S localhost:8000 web/app.php
Vous pouvez ensuite lancer l’exécution via la commande run
.
(gdb) run
Starting program: /path/to/your/php/src/folder/sapi/cli/ph -args -S localhost:8000 web/app.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
PHP 7.0.26RC1 Development Server started at Mon Dec 4 20:56:26 2017
Listening on http://localhost:8000
Document root is /path/to/your/application
Press Ctrl-C to quit.
A partir de ce moment, vous pouvez utiliser gdb de manière classique : ajoutez des break points où vous le souhaitez pour déboguer directement PHP. Cet article n’a pas pour but d’expliquer le fonctionnement de gdb, je vous renvoie donc vers openclassroom si vous avez besoin d’en savoir plus : https://openclassrooms.com/courses/deboguer-son-programme-avec-gdb
Je vous l’ai dit, vous pouvez (devez !) utiliser les helpers fournis par PHP en les sourçant :
(gdb) source ~/.gdbinit
Vous avez alors accès à plusieurs commandes que vous pouvez lister avec la commande
help user-defined
(gdb) help user-defined
User-defined commands.
The commands in this class are those defined by the user.
Use the "define" command to define a command.
List of commands:
____executor_globals -- portable way of accessing executor_globals
____print_const_table -- User-defined
____print_ht -- User-defined
____print_inh_class -- User-defined
____print_inh_iface -- User-defined
____print_str -- User-defined
____printzv -- User-defined
____printzv_contents -- User-defined
dump_bt -- dumps the current execution stack
lookup_root -- lookup a refcounted in root
print_const_table -- User-defined
print_cvs -- Prints the compiled variables and their values
print_ft -- dumps a function table (HashTable)
print_ht -- dumps elements of HashTable made of zval
print_htptr -- dumps elements of HashTable made of pointers
print_htstr -- dumps elements of HashTable made of strings
print_inh -- User-defined
print_pi -- Takes a pointer to an object's property and prints the property information
print_zstr -- print the length and contents of a zend string
printzn -- print type and content of znode
printzops -- dump operands of the current opline
printzv -- prints zval contents
set_ts -- set the ts resource
zbacktrace -- prints backtrace
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.
Le help parle de lui même mais je vais détailler un peu les commandes qui me semblent le plus utile :
J’espère que cet article rendra vos debug moins pénibles dans le futur !
Notez que grâce à ce genre de débug vous pourriez potentiellement résoudre des problèmes de PHP répertoriés dans la liste des bugs. A vos claviers ! 😁