Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

php-fpm(php) をビルドして gdb でデバッグ実行できる Docker Compose 環境を作った

php-fpm と php コマンドを php-src からビルドして、gdb コマンドでデバッグ実行できる Docker Compose 環境を作りました。

github.com

利用方法

リポジトリを 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 を終了するには、quitq コマンドを実行します。

>>> q

php-fpm コマンドや php-fpm ワーカープロセスへ gdb でアタッチする方法は、gdb-php リポジトリの readme に記載しています。

GitHub - shin1x1/gdb-php

gdb コマンド

gdb には多数のコマンドがありますが一部を下記にまとめています。コマンドには省略形のコマンドがあるので、頻繁に使うものはこちらを使うのが良いでしょう。

コマンド 省略形 内容
break b ブレークポイントを設定(関数名、ファイル名:行番号など)
run r プログラム実行
next n ステップオーバー
step s ステップイン
continue c 処理を再開
print 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 の内部の挙動を確認できます。合わせることでより理解が進むので、よかったら活用してください。

blog.shin1x1.com