========== 付録B. gdb ========== B-1.gdb とは ============  C のソースコードをコンパイルすると、バイナリオブジェクトに変換されます。バイナリになった実行ファイルを C のソースレベルで追跡するツールとして、gdb が利用できます。  メモリ管理を自前で行なう必要のある C 言語で開発している以上、不正なメモリアクセスを意味する "Segmentation fault" エラーから逃れることはできません。これは "Segmentation Violation" や『メモリ保護違反』『一般保護違反』などとも呼ばれますが、以下 "SEGV" と記載します。  バイナリ実行時に SEGV 等の継続不能なエラーが発生した場合、Linux を始めとする Unix 由来の OS では、カレントディレクトリに「コアファイル」と呼ばれるメモリダンプファイルが生成されます。これを gdb で読み込むことにより、ソースコードレベルでエラーの発生箇所を特定できます。  この章では、gdb の使い方について簡単にご紹介します。 B-2.利用準備 ============  gdb はデフォルトではインストールされていないかもしれませんが、 `推奨環境 `_ に従った場合、自動でインストールされるようにしています。入っていない場合は以下のコマンドでインストールしてください。 .. code-block:: bash $ sudo yum install gdb  ~/.gdbinit というファイルが存在する場合、gdb はそれを起動時にスタートアップスクリプトとして読み込みます。PHP Extension を開発する場合、 `ひな形の作成 `_ で使用した `ext_skel` がサンプルの ~/php/.gdbinit を生成しますので、これをホームディレクトリにコピーしておきます。 .. code-block:: bash $ cp ~/php/.gdbinit ~  SEGV によって作られるコアファイルは一般ユーザーにとっては邪魔なだけなので、最近はデフォルトではコアファイルを作らないようになっています。この機能は Extension 開発には必要なので、シェルで有効にしておきます。 .. code-block:: bash :emphasize-lines: 1,3,4 $ 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 によるデバッグの例を示します。 .. code-block:: bash :emphasize-lines: 1,3 $ php g.php Segmentation fault (コアダンプ) $ ls core* core.4799  SEGV が発生すると、カレントディレクトリに "core.PID" というファイル名でコアファイルが生成されます。俗に言う『コアを吐く』という現象です。PID はプロセス ID で、実行のたびに異なる値になります。コアファイルの中身は、SEGV でプロセスが強制終了された時点のプロセス内部のメモリ内容そのものです。これを利用して SEGV が発生した箇所を特定できます。  gdb は "gdb PHPバイナリ [コアファイル名]" で起動します。 .. code-block:: bash :emphasize-lines: 1 $ 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 の起動時に、以下のように表示される場合があります。 .. code-block:: bash 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 でライブラリをインストールします。 .. code-block:: bash $ 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 で止まった時点での呼び出しシーケンスを表示しています。 .. code-block:: bash :emphasize-lines: 1 (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 to continue, or q 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 スクリプトを書いてみました。 .. code-block:: bash :emphasize-lines: 1 $ cat -n simple-copy.php 1 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 to continue, or q 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() でオペコードを逐次実行しているようです。 .. code-block:: bash :emphasize-lines: 1 (gdb) p file_count $6 = 3  PHP スクリプトファイルは 1 個しかないのに、ファイル数は 3 が渡ってきています。追ってみると、最初と最後のファイルは空のようでスキップされたため、 `auto_prepend_file `_ と `auto_append_file `_ の処理を行っているのではないかと思われます。  コンパイル処理はとりあえず実行時の追跡には関係ないので、zend_execute() の中に入っていきます。 B-6.zend_execute() ================== .. code-block:: bash :emphasize-lines: 1,3,9 (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() のようです。 .. code-block:: bash :emphasize-lines: 1 (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 で探してみると、これは関数ポインタで、実体は以下のところに定義がありました。 .. code-block:: bash :emphasize-lines: 1 ~/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);  変数なので、以下で定義できるはずです。 .. code-block:: bash :emphasize-lines: 1 (gdb) b *zend_execute_ex Breakpoint 3 at 0x7fffed695dbb: file /home/vagrant/xdebug/xdebug.c, line 1590.  後から追加で導入した xdebug の中にブレークポイントが設定されてしまいました。この環境では追跡が困難になりそうなので、いったん Xdebug Extension の組み込みを解除してから、再度挑戦します。 .. code-block:: bash :emphasize-lines: 1,7,8 (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() ===================== .. code-block:: bash :emphasize-lines: 1,3,5 $ 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 に値が設定されていないので、ブレークポイントとしては使えないようです。いったんブレークポイントを解除します。 .. code-block:: bash :emphasize-lines: 1,4 (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000000000 (gdb) delete 1 info ---- 各種のステータスを表示します。引数として表示したい属性を指定します。 delete ------ ブレークポイントを削除します。引数として削除したいブレークポイントの番号を指定します。  いったん zend_execute() に入り、`*zend_execute_ex` に値が入っているのを確認後、再度ブレークポイントを設定します。 .. code-block:: bash :emphasize-lines: 1,3,5,13,15,17 $ 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 (gdb) p *zend_execute_ex $2 = {void (zend_execute_data *)} 0x939982 (gdb) c Continuing. [Inferior 1 (process 4523) exited normally]  プロセスが正常終了してしまいました。どうもうまくいきません。しかたがないので今度は行番号で挑戦してみます。 .. code-block:: bash :emphasize-lines: 1,2,4,12,14 (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);  今度はうまく止まってくれたようです。中に入ってみます。 .. code-block:: bash :emphasize-lines: 1,4 (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 ~  以下のようなコマンドが用意されています。興味のあるかは試してみてください。 .. list-table:: :widths: 5 15 80 :header-rows: 1 * - 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 * - 12 - printzn - | znode の型とその中身を表示します。 | 使用法:printzn &opline->op1 * - 13 - printzops - 現在の opline のオペランドをダンプします。 * - 14 - print_zstr - | zend_string の長さとその中身を表示します。 | 使用法:print_zstr [最大長] * - 15 - zbacktrace - | バックトレースを表示します。 | このコマンドは、おおむね以下のショートカットです。 | > (gdb) ____executor_globals | > (gdb) dump_bt $eg.current_execute_data * - 16 - lookup_root - | ルートにおける refcounted を検索します。 | 使用法:lookup_root [ptr]