GDBデバッガを利用してPHP内部の動きを知る - PHPソースコードリーディング入門その2

前回: PHPソースコードリーディング入門(とっかかり編) - id:anatooのブログ

PHPソースコードを読んでいく際に、どうしてもソースコードを読むだけではよくわからない部分というのが出てくる。この記事ではPHPをデバッガで動かして内部の働きを明らかにする方法を書く。

ソースコードの取得

gitから取ってくる。

$ git clone https://github.com/php/php-src.git

デバッガで動かせるようにビルドする

余計な拡張は無しで、デバッガで動かせるようにビルドする。configure時に--enable-debugオプションを渡す。

$ cd php-src
$ ./buildconf
$ ./configure --disable-all --enable-debug
$ make

GDBで動かす

makeした後、コマンドラインで動かせるバイナリはsapi/cli/phpにあるので以下のように起動する。まあ普通のGDBの使い方。

$ gdb sapi/cli/php

後はGDBのマニュアルなどを見つつ存分にデバッガを扱う。自分の場合は直接GDBを使わずにCGDBというGDBフロントエンドを利用しているがこのへんは好き好きで変える。

PHPの組み込み関数内にブレークポイントを設定する

PHPの組み込み関数や拡張で宣言されるPHPの関数は、PHP内部ではZEND_FUNCTIONというマクロを用いて宣言される。ZEND_FUNCITONマクロの中身を調べると、これらの関数は、実際には"zif_"というプレフィックスを付けられてC言語レベルでの関数として宣言されている。zifとは、zend internal functionの略と思われる。

実際にgdbPHPの組み込み関数にブレークポイントを設定する例が以下。ここではstrlen関数にブレークポイントを張っている。

$ gdb sapi/cli/php
(gdb) break zif_strlen
Breakpoint 1 at 0x10022c759: file /path/to/myphp/Zend/zend_builtin_functions.c, line 482.
(gdb) run -r "echo strlen('hoge');"
Starting program: /path/to/myphp/sapi/cli/php -r "echo strlen(0);"
Reading symbols for shared libraries ++. done

Breakpoint 1, zif_strlen (ht=1, return_value=0x10064bfe0, return_value_ptr=0x0, this_ptr=0x0, return_value_used=1) at /path/to/myphp/Zend/zend_builtin_functions.c:482

CGDBだと以下みたいな画面になる。ZEND_FUNCTION(strlen)で宣言されたコードの中で止まっているのがわかる。


PHP用のGDBカスタムコマンドを利用する

PHPソースコードには、GDBで利用できるPHP用のカスタムコマンドも含まれている。ソースコードリポジトリ直下の".gdbinit"というファイルがそれ。このカスタムコマンドでは、PHPをデバッガで動かす際に便利なコマンドが幾つか収められている。

このカスタムコマンド利用するには、.gdbinitがあるディレクトリ上でGDBを起動するか、もしくはホームディレクトリに.gdbinitをコピーしてGDBを起動する。もしくはGDBの--commandオプションで.gdbinitファイルのパスを渡す。以下のように。

$ gdb --command .gdbinit sapi/cli/php

以下は、.gdbinitに収められているカスタムコマンドの一部である。

コマンド名 概要
printzv zval構造体ポインタの中身をわかりやすく表示する。zval構造体とは、PHPで扱う値のC言語レベルでの表現。
zbacktrace GDBのbacktraceコマンドがC言語のコールスタックを表示するのに対して、zbackraceコマンドはPHPレベルでのコールスタックを表示する

ここで挙げたカスタムコマンド以外も.gdbinitには含まれているので覗いてみるとよい。現在の実装に追いついておらず使えないコマンドもいくつかあるようなので注意。

php-fpmをデバッガとともに動かす

コマンドラインで動かすPHP以外にも、実際にウェブアプリとして動いているPHPアプリケーションをデバッグしたい場合は、php-fpmの子プロセスの数を一つに設定した後起動し、プロセスIDを調べてデバッガでattachする。attachした直後は、php-fpmはブレークした状態になっている。ブレークポイントを設定した後、continueコマンドでブレーク状態から抜けてブラウザからPHPを実行するとブレークポイントを張ったところで止まってくれる。

例えば、cliでのデバッガの使用例と同じく、PHPで書いたウェブアプリ内の組み込み関数内でブレークしたい場合は、以下のようにすればよい。

$ # php-fpmの子プロセス数を1に設定する
$ vim php-fpm.conf 
(中略)
pm = static
pm.max_children = 1
(中略)
$ php-fpm
$ ps A | grep php-fpm
58090   ??  Ss     0:00.01 sapi/fpm/php-fpm
58091   ??  S      0:00.00 sapi/fpm/php-fpm
58095 s000  R+     0:00.00 grep php-fpm
$ # 二番目のプロセスIDを指定してアタッチ
$ gdb --pid 58091
(gdb) break zif_strlen
(gdb) continue

それで、以下のようなスクリプトにブラウザからアクセスすれば、ブレークされる。ので、後はcliのバイナリをデバッガで動かす場合と同じ。

<?php echo strlen('hoge');

apacheモジュールで動かす場合は知らない。ググれば多分普通に出てくるはず。

終わり

この記事では、どのようにGDBデバッガでPHPの内部の働きを理解するためのとっかかりを記述した。次は、PHPの関数が拡張ライブラリによってどのように宣言されているか、またそのコードをどのように読んでいけば良いのかについて書く。