付録B. gdb

B-1.gdb とは

 C のソースコードをコンパイルすると、バイナリオブジェクトに変換されます。バイナリになった実行ファイルを C のソースレベルで追跡するツールとして、gdb が利用できます。

 メモリ管理を自前で行なう必要のある C 言語で開発している以上、不正なメモリアクセスを意味する "Segmentation fault" エラーから逃れることはできません。これは "Segmentation Violation" や『メモリ保護違反』『一般保護違反』などとも呼ばれますが、以下 "SEGV" と記載します。

 バイナリ実行時に SEGV 等の継続不能なエラーが発生した場合、Linux を始めとする Unix 由来の OS では、カレントディレクトリに「コアファイル」と呼ばれるメモリダンプファイルが生成されます。これを gdb で読み込むことにより、ソースコードレベルでエラーの発生箇所を特定できます。

 この章では、gdb の使い方について簡単にご紹介します。

B-2.利用準備

 gdb はデフォルトではインストールされていないかもしれませんが、 推奨環境 に従った場合、自動でインストールされるようにしています。入っていない場合は以下のコマンドでインストールしてください。

$ sudo yum install gdb

 ~/.gdbinit というファイルが存在する場合、gdb はそれを起動時にスタートアップスクリプトとして読み込みます。PHP Extension を開発する場合、 ひな形の作成 で使用した ext_skel がサンプルの ~/php/.gdbinit を生成しますので、これをホームディレクトリにコピーしておきます。

$ cp ~/php/.gdbinit ~

 SEGV によって作られるコアファイルは一般ユーザーにとっては邪魔なだけなので、最近はデフォルトではコアファイルを作らないようになっています。この機能は Extension 開発には必要なので、シェルで有効にしておきます。

$ vi ~/.bashrc
ulimit -c unlimited   ←  なければこの行を追加
$ . ~/.bashrc         ←  カレントシェルに即時反映
$ ulimit -a           ←  動作確認
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7276
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

 "core file size" が unlimited になっていれば OK です。

B-3.コアファイルの利用例

 以下に、SEGV が発生した際の、gdb によるデバッグの例を示します。

$ php g.php
Segmentation fault (コアダンプ)
$ ls core*
core.4799

 SEGV が発生すると、カレントディレクトリに "core.PID" というファイル名でコアファイルが生成されます。俗に言う『コアを吐く』という現象です。PID はプロセス ID で、実行のたびに異なる値になります。コアファイルの中身は、SEGV でプロセスが強制終了された時点のプロセス内部のメモリ内容そのものです。これを利用して SEGV が発生した箇所を特定できます。

 gdb は "gdb PHPバイナリ [コアファイル名]" で起動します。

$ gdb php core.4799
Reading symbols from /usr/local/bin/php...done.
[New LWP 4799]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".
Core was generated by `php g.php\'.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000008a1f22 in zend_mm_free_heap (heap=0x7f1e93600040,
    ptr=0x31cfde0,
    __zend_filename=0x7f1e8ced4410 "/home/vagrant/php/ext/aha/aha.c",
    __zend_lineno=550, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /home/vagrant/php/Zend/zend_alloc.c:1372
1372                    zend_mm_page_info info = chunk->map[page_num];

 gdb の起動時に、以下のように表示される場合があります。

Missing separate debuginfos, use: debuginfo-install keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.14.1-27.el7_3.x86_64 libcom_err-1.42.9-9.el7.x86_64 libselinux-2.5-6.el7.x86_64 ncurses-libs-5.9-13.20130511.el7.x86_64 openssl-libs-1.0.1e-60.el7_3.1.x86_64 pcre-8.32-15.el7_2.1.x86_64 readline-6.2-9.el7.x86_64

 この場合、システムライブラリがデバッグ情報が含まれない状態でインストールされていますので、指示通り debuginfo-install でライブラリをインストールします。

$ sudo debuginfo-install keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.14.1-27.el7_3.x86_64 libcom_err-1.42.9-9.el7.x86_64 libselinux-2.5-6.el7.x86_64 ncurses-libs-5.9-13.20130511.el7.x86_64 openssl-libs-1.0.1e-60.el7_3.1.x86_64 pcre-8.32-15.el7_2.1.x86_64 readline-6.2-9.el7.x86_64

 gdb にはさまざまコマンドが用意されていますが、ここでは bt(backtrace) コマンドにより、SEGV で止まった時点での呼び出しシーケンスを表示しています。

(gdb) bt
#0  0x00000000008a1f22 in zend_mm_free_heap (heap=0x7f1e93600040,
    ptr=0x31cfde0,
    __zend_filename=0x7f1e8ced4410 "/home/vagrant/php/ext/aha/aha.c",
    __zend_lineno=550, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /home/vagrant/php/Zend/zend_alloc.c:1372
#1  0x00000000008a47d8 in _efree (ptr=0x31cfde0,
    __zend_filename=0x7f1e8ced4410 "/home/vagrant/php/ext/aha/aha.c",
    __zend_lineno=550, __zend_orig_filename=0x0, __zend_orig_lineno=0)
    at /home/vagrant/php/Zend/zend_alloc.c:2433
#2  0x00007f1e8ced3a93 in zif_aha_MbStatusReceive (
    execute_data=0x7f1e936140c0, return_value=0x7f1e93614090)
    at /home/vagrant/php/ext/aha/aha.c:550
#3  0x000000000093a2a8 in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER ()
    at /home/vagrant/php/Zend/zend_vm_execute.h:675
#4  0x00000000009399cb in execute_ex (ex=0x7f1e93614030)
    at /home/vagrant/php/Zend/zend_vm_execute.h:429
#5  0x0000000000939add in zend_execute (op_array=0x7f1e9367e000,
    return_value=0x0) at /home/vagrant/php/Zend/zend_vm_execute.h:474
#6  0x00000000008db17c in zend_execute_scripts (type=8, retval=0x0,
    file_count=3) at /home/vagrant/php/Zend/zend.c:1476
#7  0x0000000000848f91 in php_execute_script (primary_file=0x7fffd73a5520)
    at /home/vagrant/php/main/main.c:2537
#8  0x00000000009baaae in do_cli (argc=2, argv=0x31cdba0)
---Type <return> to continue, or q <return> to quit---
    at /home/vagrant/php/sapi/cli/php_cli.c:993
#9  0x00000000009bba6d in main (argc=2, argv=0x31cdba0)
    at /home/vagrant/php/sapi/cli/php_cli.c:1381

B-4.gdb のコマンド

  参照カウント法 のところで出てきた gc.refcount の動きを確かめようと思って、以下のような PHP スクリプトを書いてみました。

$ cat -n simple-copy.php
   1  <?php
   2  $a = "new string";
   3  $b = $a;

 これを例にして、gdb の使い方を見てみましょう。gdb の中で使えるコマンドは多数ありますが、ここでは新しいコマンドが出てくるたびに、逐次簡単な書式説明をしています。ここに出てこないコマンドは help で探してみてください。

 まず、単に php を引数として gdb を起動します。

$ gdb php
Reading symbols from /usr/local/bin/php...done.

 main() 関数の先頭にブレークポイントを設定します。

(gdb)  b main
Breakpoint 1 at 0x9bb3f1: file /home/vagrant/php/sapi/cli/php_cli.c, line 1198.

 main 関数のパスが表示されていますので、このソースファイルを別ウィンドウで表示しておくと理解が進みます。

break(b)

ブレークポイントを設定します。引数として関数名または [ソースファイル名:]行番号 を指定できます。ソースファイル名が省略されると、現在のソースが使われます。

動かしたい PHP スクリプトを指定して実行を開始します。

(gdb) run simple-copy.php
Starting program: /usr/local/bin/php simple-copy.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".

Breakpoint 1, main (argc=2, argv=0x7fffffffe538)
    at /home/vagrant/php/sapi/cli/php_cli.c:1198
1198            int exit_status = SUCCESS;

run

プログラムの実行を開始します。引数はコマンドラインで指定する引数と同じです。ブレークポイントに到達するまで実行を継続します。

 現在のプロセスの実行状態を表示します。

(gdb) bt
#0  main (argc=2, argv=0x7fffffffe538)
    at /home/vagrant/php/sapi/cli/php_cli.c:1198
(gdb) p *argv
$3 = 0x7fffffffe76f "/usr/local/bin/php"
(gdb) p argv[0]
$4 = 0x7fffffffe76f "/usr/local/bin/php"
(gdb) p argv[1]
$5 = 0x7fffffffe782 "simple-copy.php"

backtrace(bt)

バックトレースを表示します。

list(l)

現在行周辺のソースコードを表示します。

 list(l) で表示されるのは、php バイナリの元になっている C のコードです。php バイナリから見ると simple-copy.php は単なるデータファイルに過ぎないので、list コマンドで中身を表示することはできません。ただし、シェルを経由して間接的に表示することはできます。

(gdb) !cat simple-copy.php
<?php
$a = "new string";
$b = $a;

!COMMAND

シェルのコマンドを呼び出す。
(gdb) n
1199            int module_started = 0, sapi_started = 0;

next(n)

ソースコードレベルで1行実行します。関数の中には入りません。
(gdb) b 1212
Breakpoint 2 at 0x9bb445: file /home/vagrant/php/sapi/cli/php_cli.c, line 1212.
(gdb) c
Continuing.

Breakpoint 2, main (argc=2, argv=0x7fffffffe538)
    at /home/vagrant/php/sapi/cli/php_cli.c:1212
1212            argv = save_ps_args(argc, argv);

continue(c)

次のブレークポイントまで、一気に実行します。
(gdb) b do_cli
Breakpoint 3 at 0x9ba05c: file /home/vagrant/php/sapi/cli/php_cli.c, line 664.
(gdb) c
Continuing.

Breakpoint 3, do_cli (argc=2, argv=0x1296bc0)
    at /home/vagrant/php/sapi/cli/php_cli.c:664
664             int behavior = PHP_MODE_STANDARD;

 適当にあたりをつけて、do_cli() まで進みました。中のソースを確認します。

(gdb) b 984
Breakpoint 4 at 0x9baa2c: file /home/vagrant/php/sapi/cli/php_cli.c, line 984.
(gdb) c
Continuing.

Breakpoint 4, do_cli (argc=2, argv=0x1296bc0)
    at /home/vagrant/php/sapi/cli/php_cli.c:984
984                     switch (behavior) {
(gdb) l
979                     }
980
981                     zend_is_auto_global_str(ZEND_STRL("_SERVER"));
982
983                     PG(during_request_startup) = 0;
984                     switch (behavior) {
985                     case PHP_MODE_STANDARD:
986                             if (strcmp(file_handle.filename, "-")) {
987                                     cli_register_file_handles();
988                             }

 981 行目の zend_is_auto_global_str(ZEND_STRL("_SERVER")) は、"_SERVER" スーパーグローバル変数を登録に行っているのではないかと想像できます。

(gdb) p behavior
$1 = 1
(gdb) p PHP_MODE_STANDARD
No symbol "PHP_MODE_STANDARD" in current context.

 C のソース上は PHP_MODE_STANDARD となっていても、 #define で定義されたマクロはプリプロセッサ(コンパイルの前処理)により実際の値に置き換えられてからコンパイラに渡されるので、gdb 上では参照できません。

(gdb) n
986                             if (strcmp(file_handle.filename, "-")) {

 PHP_MODE_STANDARD のブロックに入りました。

(gdb) l 984,996
984                     switch (behavior) {
985                     case PHP_MODE_STANDARD:
986                             if (strcmp(file_handle.filename, "-")) {
987                                     cli_register_file_handles();
988                             }
989
990                             if (interactive && cli_shell_callbacks.cli_shell_run) {
991                                     exit_status = cli_shell_callbacks.cli_shell_run();
992                             } else {
993                                     php_execute_script(&file_handle);
994                                     exit_status = EG(exit_status);
995                             }
996                             break;

 ここがメイン部分のようです。list 行番号,行番号 で指定範囲のソースを表示できます。注意深く1行ずつ実行します。

(gdb) n
987                                     cli_register_file_handles();
(gdb) n
990                             if (interactive && cli_shell_callbacks.cli_shell_run) {
(gdb) n
993                                     php_execute_script(&file_handle);

 ここでスクリプトを実行しているようです。中に入ってみます。

(gdb) s
php_execute_script (primary_file=0x7fffffffe180)
    at /home/vagrant/php/main/main.c:2446
2446            zend_file_handle prepend_file = {{0}, NULL, NULL, 0, 0}, append_file = {{0}, NULL, NULL, 0, 0};

step(s)

関数の中に入る。

 ソースファイルが main.c に切り替わりました。逐次エディタで該当箇所のソースコードを表示しながら追っていきます。

(gdb) l 2528,2538
2528                    if (CG(start_lineno) && prepend_file_p) {
2529                            int orig_start_lineno = CG(start_lineno);
2530
2531                            CG(start_lineno) = 0;
2532                            if (zend_execute_scripts(ZEND_REQUIRE, NULL, 1, prepend_file_p) == SUCCESS) {
2533                                    CG(start_lineno) = orig_start_lineno;
2534                                    retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 2, primary_file, append_file_p) == SUCCESS);
2535                            }
2536                    } else {
2537                            retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);
2538                    }

 ここにある zend_execute_scripts() がメインの実行ルーチンのようです。2箇所ありますので、その手前まで実行して、入り口を特定します。

(gdb) b 2528
Breakpoint 3 at 0x848eb2: file /home/vagrant/php/main/main.c, line 2528.
(gdb) c
Continuing.

Breakpoint 3, php_execute_script (primary_file=0x7fffffffe180)
    at /home/vagrant/php/main/main.c:2528
2528                    if (CG(start_lineno) && prepend_file_p) {
(gdb) n
2537                            retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);

 入り口が特定できました。中に入ってみます。

(gdb) s
zend_execute_scripts (type=8, retval=0x0, file_count=3)
    at /home/vagrant/php/Zend/zend.c:1463
1463            va_start(files, file_count);

 Zend ディレクトリ内に入りました。ここからが Zend Engine のようです。いったん gdb を終了します。

(gdb) q
A debugging session is active.

        Inferior 1 [process 4457] will be killed.

Quit anyway? (y or n) y
quit(q)
gdb を終了します。

B-5.zend_execute_scripts()

 利用するスクリプトを再掲します。

$ cat -n simple-copy.php
   1  <?php
   2  $a = "new string";
   3  $b = $a;

 それでは、最初から実行してみます。ちなみに gdb では readline が使えますので、上下矢印キー(↑↓)で過去のコマンド履歴が呼び出せます。

$ gdb php
Reading symbols from /usr/local/bin/php...done.
(gdb) b zend_execute_scripts
Breakpoint 1 at 0x8db058: file /home/vagrant/php/Zend/zend.c, line 1463.
(gdb) run simple-copy.php
Starting program: /usr/local/bin/php simple-copy.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".

Breakpoint 1, zend_execute_scripts (type=8, retval=0x0, file_count=3)
    at /home/vagrant/php/Zend/zend.c:1463
1463            va_start(files, file_count);

メインの関数は数十行程度です。

(gdb) l 1456,1492
1456    ZEND_API int zend_execute_scripts(int type, zval *retval, int file_count, ...) /* {{{ */
1457    {
1458            va_list files;
1459            int i;
1460            zend_file_handle *file_handle;
1461            zend_op_array *op_array;
1462
1463            va_start(files, file_count);
1464            for (i = 0; i < file_count; i++) {
1465                    file_handle = va_arg(files, zend_file_handle *);
1466                    if (!file_handle) {
1467                            continue;
1468                    }
1469
1470                    op_array = zend_compile_file(file_handle, type);
1471                    if (file_handle->opened_path) {
1472                            zend_hash_add_empty_element(&EG(included_files), file_handle->opened_path);
1473                    }
1474                    zend_destroy_file_handle(file_handle);
1475                    if (op_array) {
1476                            zend_execute(op_array, retval);
---Type <return> to continue, or q <return> to quit---
1477                            zend_exception_restore();
1478                            zend_try_exception_handler();
1479                            if (EG(exception)) {
1480                                    zend_exception_error(EG(exception), E_ERROR);
1481                            }
1482                            destroy_op_array(op_array);
1483                            efree_size(op_array, sizeof(zend_op_array));
1484                    } else if (type==ZEND_REQUIRE) {
1485                            va_end(files);
1486                            return FAILURE;
1487                    }
1488            }
1489            va_end(files);
1490
1491            return SUCCESS;
1492    }

 for() ループで複数の PHP スクリプトファイルをひとつずつ処理しています。1470 行目の zend_compile_file() でファイルをコンパイルしてオペコードに変換し、1476 行目の zend_execute() でオペコードを逐次実行しているようです。

(gdb) p file_count
$6 = 3

 PHP スクリプトファイルは 1 個しかないのに、ファイル数は 3 が渡ってきています。追ってみると、最初と最後のファイルは空のようでスキップされたため、 auto_prepend_fileauto_append_file の処理を行っているのではないかと思われます。

 コンパイル処理はとりあえず実行時の追跡には関係ないので、zend_execute() の中に入っていきます。

B-6.zend_execute()

(gdb) b zend_execute
Breakpoint 2 at 0x9399fb: file /home/vagrant/php/Zend/zend_vm_execute.h, line 461.
(gdb) c
Continuing.

Breakpoint 2, zend_execute (op_array=0x7ffff3e7f000, return_value=0x0)
    at /home/vagrant/php/Zend/zend_vm_execute.h:461
461             if (EG(exception) != NULL) {
(gdb) l 457,476
457     ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
458     {
459             zend_execute_data *execute_data;
460
461             if (EG(exception) != NULL) {
462                     return;
463             }
464
465             execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,
466                     (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data))  );
467             if (EG(current_execute_data)) {
468                     execute_data->symbol_table = zend_rebuild_symbol_table();
469             } else {
470                     execute_data->symbol_table = &EG(symbol_table);
471             }
472             EX(prev_execute_data) = EG(current_execute_data);
473             i_init_execute_data(execute_data, op_array, return_value);
474             zend_execute_ex(execute_data);
475             zend_vm_stack_free_call_frame(execute_data);
476     }

 ざっと見る限り、実際に実行データを実行しているのは 474 行目の zend_execute_ex() のようです。

(gdb) b zend_execute_ex
Function "zend_execute_ex" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

 どうも zend_execute_ex() は単なる関数ではないようで、gdb からは見えなくなっており、ブレークポイントが設定できません。grep で探してみると、これは関数ポインタで、実体は以下のところに定義がありました。

~/php$ grep -rI zend_execute_ex . | grep ZEND_API | grep -v extern
./Zend/zend_execute_API.c:ZEND_API void (*zend_execute_ex)(zend_execute_data *execute_data);

 変数なので、以下で定義できるはずです。

(gdb) b *zend_execute_ex
Breakpoint 3 at 0x7fffed695dbb: file /home/vagrant/xdebug/xdebug.c, line 1590.

 後から追加で導入した xdebug の中にブレークポイントが設定されてしまいました。この環境では追跡が困難になりそうなので、いったん Xdebug Extension の組み込みを解除してから、再度挑戦します。

(gdb) q
A debugging session is active.

        Inferior 1 [process 4476] will be killed.

Quit anyway? (y or n) y
$ sudo vi /usr/local/lib/php.ini
$ cat /usr/local/lib/php.ini
extension=/home/vagrant/php/ext/my_ext/modules/my_ext.so
# zend_extension=/home/vagrant/xdebug/modules/xdebug.so

B-7.zend_execute_ex()

$ gdb php
Reading symbols from /usr/local/bin/php...done.
(gdb) b *zend_execute_ex
Breakpoint 1 at 0x0
(gdb) run simple-copy.php
Starting program: /usr/local/bin/php simple-copy.php
Warning:
Cannot insert breakpoint 1.
Error accessing memory address 0x0: 入力/出力エラーです.

 起動直後は zend_execute_ex に値が設定されていないので、ブレークポイントとしては使えないようです。いったんブレークポイントを解除します。

(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000000000
(gdb) delete 1

info

各種のステータスを表示します。引数として表示したい属性を指定します。

delete

ブレークポイントを削除します。引数として削除したいブレークポイントの番号を指定します。

 いったん zend_execute() に入り、*zend_execute_ex に値が入っているのを確認後、再度ブレークポイントを設定します。

$ gdb php
Reading symbols from /usr/local/bin/php...done.
(gdb) b zend_execute
Breakpoint 1 at 0x9399fb: file /home/vagrant/php/Zend/zend_vm_execute.h, line 461.
(gdb) run simple-copy.php
Starting program: /usr/local/bin/php simple-copy.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".

Breakpoint 1, zend_execute (op_array=0x7ffff3e7e000, return_value=0x0)
    at /home/vagrant/php/Zend/zend_vm_execute.h:461
461             if (EG(exception) != NULL) {
(gdb) p zend_execute_ex
$1 = (void (*)(zend_execute_data *)) 0x939982 <execute_ex>
(gdb) p *zend_execute_ex
$2 = {void (zend_execute_data *)} 0x939982 <execute_ex>
(gdb) c
Continuing.
[Inferior 1 (process 4523) exited normally]

 プロセスが正常終了してしまいました。どうもうまくいきません。しかたがないので今度は行番号で挑戦してみます。

(gdb) delete 1
(gdb) b zend_execute
Breakpoint 2 at 0x9399fb: file /home/vagrant/php/Zend/zend_vm_execute.h, line 461.
(gdb) run simple-copy.php
Starting program: /usr/local/bin/php simple-copy.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".

Breakpoint 2, zend_execute (op_array=0x7ffff3e7e000, return_value=0x0)
    at /home/vagrant/php/Zend/zend_vm_execute.h:461
461             if (EG(exception) != NULL) {
(gdb) b 474
Breakpoint 3 at 0x939aca: file /home/vagrant/php/Zend/zend_vm_execute.h, line 474.
(gdb) c
Continuing.

Breakpoint 3, zend_execute (op_array=0x7ffff3e7e000, return_value=0x0)
    at /home/vagrant/php/Zend/zend_vm_execute.h:474
474             zend_execute_ex(execute_data);

 今度はうまく止まってくれたようです。中に入ってみます。

(gdb) s
execute_ex (ex=0x7ffff3e14030) at /home/vagrant/php/Zend/zend_vm_execute.h:411
411             const zend_op *orig_opline = opline;
(gdb) bt
#0  execute_ex (ex=0x7ffff3e14030)
    at /home/vagrant/php/Zend/zend_vm_execute.h:411
#1  0x0000000000939add in zend_execute (op_array=0x7ffff3e7e000,
    return_value=0x0) at /home/vagrant/php/Zend/zend_vm_execute.h:474
#2  0x00000000008db17c in zend_execute_scripts (type=8, retval=0x0,
    file_count=3) at /home/vagrant/php/Zend/zend.c:1476
#3  0x0000000000848f91 in php_execute_script (primary_file=0x7fffffffe180)
    at /home/vagrant/php/main/main.c:2537
#4  0x00000000009baaae in do_cli (argc=2, argv=0x1296bc0)
    at /home/vagrant/php/sapi/cli/php_cli.c:993
#5  0x00000000009bba6d in main (argc=2, argv=0x1296bc0)
    at /home/vagrant/php/sapi/cli/php_cli.c:1381

 zend_execute() の直前に定義のある、 execute_ex() という関数(のようなもの?)の中で止まりました。ここが Zend Engine の中心のようですが、かなり難解なので、筆者の実力では gdb で追えるのはここまでのようです。ちなみに zend_vm_execute.h は6万ステップ以上あります。

 Zend Engine のコアを追いかけるのは C 言語に精通していないと難しそうですが、PHP Extension の中を追跡する程度であれば gdb は非常に役に立ちますので、ぜひ使ってみてください。

B-8. Extension 開発用の .gdbinit

 ~/php (PHPのソースツリー)直下に .gdbinit が用意されています。これをホームディレクトリにコピーすることで、gdb 上でさまざまな機能が使えるようになります。:

$ cp ~/php/.gdbinit ~

 以下のようなコマンドが用意されています。興味のあるかは試してみてください。

No. コマンド名 説明
1 set_ts
リソースをセットします。プロセスが走っていない場合に gdb が
ts_resource_ex を呼び出すために重要です。ただし、フレーム
情報の引数からリソースを取得できる場合もあります。
2 print_cvs
コンパイル後の変数とその値を表示します。zend_execute_data が
セットされている場合、そのスコープの範囲にあるコンパイル後の
値を表示します。パラメータが指定されない場合、スコープとして
current_execute_data を使用します。
使用法:print_cvs [zend_execute_data *]
3 dump_bt
現在の実行スタックをダンプします。
使用法:dump_bt executor_globals.current_execute_data
4 printzv zval の中身を表示します。
5 print_const_table 定数テーブルを表示します。
6 print_ht zval から作られた HashTable の要素をダンプします。
7 print_htptr ポインタから作られた HashTable の要素をダンプします。
8 print_htstr 文字列から作られた HashTable の要素をダンプします。
9 print_ft 関数テーブル( HashTable )をダンプします。
10 print_inh クラスの継承関係を表示します(?)
11 print_pi
引数としてオブジェクトのプロパティへのポインタを受け取り、
プロパティの情報を表示します。
使用法:print_pi <ptr>
12 printzn
znode の型とその中身を表示します。
使用法:printzn &opline->op1
13 printzops 現在の opline のオペランドをダンプします。
14 print_zstr
zend_string の長さとその中身を表示します。
使用法:print_zstr <ptr> [最大長]
15 zbacktrace
バックトレースを表示します。
このコマンドは、おおむね以下のショートカットです。
> (gdb) ____executor_globals
> (gdb) dump_bt $eg.current_execute_data
16 lookup_root
ルートにおける refcounted を検索します。
使用法:lookup_root [ptr]