php-fpm と php コマンドを php-src からビルドして、gdb コマンドでデバッグ実行できる Docker Compose 環境を作りました。
利用方法
リポジトリを git コマンドでチェックアウトして、make
コマンドを実行します。make コマンドを実行すると、php-src からソースをチェックアウトして、Debian の Docker コンテナで php-fpm と php コマンドをビルドします。
$ make # ビルド完了後 $ docker compose run --rm php-fpm /php-src/php/bin/php -v PHP 8.3.8-dev (cli) (built: Jun 1 2024 04:00:02) (NTS DEBUG) Copyright (c) The PHP Group Zend Engine v4.3.8-dev, Copyright (c) Zend Technologies
make php
コマンドを実行すると、gdb で php コマンドにアタッチします。
$ make gdb-php docker compose run --rm php-fpm gdb /php-src/php/bin/php GNU gdb (Debian 13.1-3) 13.1 Copyright (C) 2023 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from /php-src/php/bin/php... >>>
このコンテナには下記の PHP コードが /app/hello.php
として含まれているので、var_dump() にブレークポイントを設定して実行してみましょう。
<?php var_dump("Hello");
var_dump() にブレークポイントを仕掛けて、ここで処理が停止するようにします。ブレークポイントは break
もしくは b
コマンドに停止する箇所(関数名、ファイル名: 行番号など)を指定します。
PHP 関数は内部的には下記のように定義されており、実際の関数名には zif_var_dump
のように zif_
がプレフィックスとして付いています。
https://github.com/php/php-src/blob/PHP-8.3.8/ext/standard/var.c#L218
PHP_FUNCTION(var_dump)
PHP_FUNCTION マクロは下記のように定義されています。
// https://github.com/php/php-src/blob/PHP-8.3.8/main/php.h#L369 #define PHP_FUNCTION ZEND_FUNCTION // https://github.com/php/php-src/blob/PHP-8.3.8/Zend/zend_API.h#L72 #define ZEND_NAMED_FUNCTION(name) void ZEND_FASTCALL name(INTERNAL_FUNCTION_PARAMETERS) #define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(zif_##name)
関数名部分のマクロを展開すると下記のようになります。
void ZEND_FASTCALL zif_var_dump(INTERNAL_FUNCTION_PARAMETERS)
つまり、zif_var_dump
をブレークポイントに設定します。
>>> b zif_var_dump Breakpoint 1 at 0x5f832e: file /php-src/ext/standard/var.c, line 224
run
もしくは r
コマンドで /app/hello.php
を指定して実行します。
>>> r /app/hello.php
実行すると、var_dump() 処理で停止します。
Breakpoint 1, zif_var_dump (execute_data=0x7ffff4e15080, return_value=0x7fffffffc0c0) at /php-src/ext/standard/var.c:224 224 ZEND_PARSE_PARAMETERS_START(1, -1) ─── Breakpoints ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── [1] break at 0x00005555559f832e in /php-src/ext/standard/var.c:224 for zif_var_dump hit 1 time ─── Stack ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── [0] from 0x00005555559f832e in zif_var_dump+16 at /php-src/ext/standard/var.c:224 [1] from 0x0000555555b3676d in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER+142 at /php-src/Zend/zend_vm_execute.h:1275 [2] from 0x0000555555bb034d in execute_ex+1959 at /php-src/Zend/zend_vm_execute.h:57212 [3] from 0x0000555555bb4baa in zend_execute+251 at /php-src/Zend/zend_vm_execute.h:61604 [4] from 0x0000555555af570c in zend_execute_scripts+376 at /php-src/Zend/zend.c:1891 [5] from 0x0000555555a4f7a8 in php_execute_script+666 at /php-src/main/main.c:2515 [6] from 0x0000555555c6fae0 in do_cli+2721 at /php-src/sapi/cli/php_cli.c:966 [7] from 0x0000555555c706da in main+835 at /php-src/sapi/cli/php_cli.c:1340 ─── Source ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 219 { 220 zval *args; 221 int argc; 222 int i; 223 !224 ZEND_PARSE_PARAMETERS_START(1, -1) 225 Z_PARAM_VARIADIC('+', args, argc) 226 ZEND_PARSE_PARAMETERS_END(); 227 228 for (i = 0; i < argc; i++) { // (snip) >>>
ここから、next
もしくは n
コマンドや step
もしくは s
コマンドでステップ実行したり、print
コマンドで変数やメモリの情報を確認したりして挙動を確認できます。
continue
もしくは c
コマンドを実行すると以降の処理が続行されます。下記では処理が最後まで実行され、文字列が出力されます。
>>> c (snip) ─── Output/messages ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── string(5) "Hello" [Inferior 1 (process 46) exited normally] >>>
gdb を終了するには、quit
や q
コマンドを実行します。
>>> q
php-fpm コマンドや php-fpm ワーカープロセスへ gdb でアタッチする方法は、gdb-php リポジトリの readme に記載しています。
gdb コマンド
gdb には多数のコマンドがありますが一部を下記にまとめています。コマンドには省略形のコマンドがあるので、頻繁に使うものはこちらを使うのが良いでしょう。
コマンド | 省略形 | 内容 |
---|---|---|
break | b | ブレークポイントを設定(関数名、ファイル名:行番号など) |
run | r | プログラム実行 |
next | n | ステップオーバー |
step | s | ステップイン |
continue | c | 処理を再開 |
p | 引数で指定した変数やメモリアドレスの値を出力 | |
finish | fin | 現在の関数が終了するまで処理を実行 |
delete | d | ブレークポイントを全て削除 |
info | i | 引数で指定した情報を出力 info locals: ローカル変数を表示 info frame: スタック(フレーム)を表示 info function FUNC_NAME: FUNC_NAME に合致する関数リストを表示 info variable VAR_NAME: VAR_NAME に合致する変数リストを表示 |
quit | q | gdb を終了 |
frame | fr | 指定したフレーム番号に移動(引数無しなら現時点のフレームを表示) |
start | エントリポイント(main)の先頭に一時的なブレークポイント設定して、run を実行 |
php-src の .gdbinit
php-src には .gdbinit が含まれており、これを読み込むことで PHP を gdb デバッグする際に便利なコマンドが有効になります。gdb-php 環境では有効になっています。
主なコマンドは下記です。
コマンド | 内容 |
---|---|
printzv | zval* を与えると内容を出力 |
print_global_vars | 引数無し。グローバル変数の内容を出力(executor_globals->symbol_table)。 |
print_const_table | HashTable を与えると内容を出力(要素を zend_constant* と想定) |
print_ht | HashTable を与えると内容を出力 |
print_htptr | HashTable を与えると内容を出力(要素をポインタと想定) |
print_ft | HashTable を与えると内容を出力(要素を zend_function* と想定) |
pritn_zstr | zend_string* を与えると内容を出力 |
zbacktrace | 引数無し。PHP コードレベルのスタックトレースを出力。 |
一部コマンドの実行例は下記になります。
- printzv
>>> printzv args [0xfffff4e151a0] (refcount=2) array: Packed(2)[0xfffff4e5b2a0]: { [0] 0 => [0xfffff4e5f248] (refcount=1) string: hello [1] 1 => [0xfffff4e5f258] (refcount=1) string: wolrd }
- print_global_vars
>>> print_global_vars Hash(9)[0xaaaaabeb3190]: { [0] "_GET" => [0xfffff4e5c200] (refcount=2) array: [1] "_POST" => [0xfffff4e5c220] (refcount=2) array: [2] "_COOKIE" => [0xfffff4e5c240] (refcount=2) array: [3] "_FILES" => [0xfffff4e5c260] (refcount=2) array: [4] "argv" => [0xfffff4e5c280] (refcount=2) array: [5] "argc" => [0xfffff4e5c2a0] long: 1 [6] "_SERVER" => [0xfffff4e5c2c0] (refcount=2) array: [7] "strings" => [0xfffff4e5c2e0] indirect: [0xfffff4e15070] (refcount=2) array: [8] "o" => [0xfffff4e5c300] indirect: [0xfffff4e15080] UNDEF }
- print_ht
>>> print_ht &executor_globals->included_files Hash(3)[0xaaaaabeb31c8]: { [0] "/app/index.php" => [0xfffff4e6c040] NULL [1] "/app/function.php" => [0xfffff4e6c060] NULL [2] "/app/class.php" => [0xfffff4e6c080] NULL }
- print_ft
>>> print_ft executor_globals->function_table Hash(1027)[0xaaaaabecc4b0]: { [0] "zend_version" => "zend_version" [1] "func_num_args" => "func_num_args" [2] "func_get_arg" => "func_get_arg" [3] "func_get_args" => "func_get_args" (snip) [1024] "cli_set_process_title" => "cli_set_process_title" [1025] "cli_get_process_title" => "cli_get_process_title" [1026] "to_lower" => "to_lower" }
- print_zstr
>>> print_zstr call->func->common->function_name string(8) "var_dump"
- zbacktrace
>>> zbacktrace [0xfffff4e15150] var_dump(array(2)[0xfffff4e151a0]) [internal function] [0xfffff4e15020] (main) /app/index.php:8
PHP関数によるダンプ
gdb では call コマンドという C 言語の関数を呼び出す機能があります。そして、PHP にはオペコードをダンプする関数があるのでこれを呼び出すことで、オペコードをダンプできます。
>>> p new_op_array $2 = (zend_op_array *) 0xfffff4e83140 >>> call zend_dump_op_array(new_op_array, 0, "", null) $_main: ; (lines=4, args=0, vars=0, tmps=1) ; () ; /app/hello.php:1-3 0000 INIT_FCALL 1 96 string("var_dump") 0001 SEND_VAL string("Hello") 1 0002 DO_ICALL 0003 RETURN int(1)
zend_ast* 型の AST は下記で文字列表現(エクスポート表現)に変換できます。
>>> call zend_ast_export("", ast, "") $24 = (zend_string *) 0xfffff4e77400 >>> print_zstr $24 string(51) "class Bar extends C1 implements I1 {\12 use T1;\12}\12"
さいごに
Linux で php-src をデバッグビルドして gdb でデバッグできる環境が欲しかったので構築しました。
下記エントリのシステムコールは外部から見た振る舞いで、gdb によるデバッグ実行では PHP の内部の挙動を確認できます。合わせることでより理解が進むので、よかったら活用してください。