やってみよう!PHPの内部構造の動きをC言語レイヤーで見る方法

GDBで実際の挙動をみて確認する

まずPHPを--enable-debugでコンパイルします。
wget http://jp2.php.net/get/php-5.4.14.tar.gz/from/jp1.php.net/mirror -O php-5.4.14.tar.gz
tar xfz php-5.4.14.tar.gz
cd php-5.4.14
./configure --disable-all --enable-debug
make
次に、gdbでphpバイナリを起動します。(gdbが入ってない場合はyum/aptなどでインストールしてください。)
gdb  sapi/cli/php
起動したら、print_rにブレークポイントを仕掛けます。
(print_rでなくても他の関数でもよいです。)
組み込み関数にはzif_という接頭辞をつけるという暗黙のルールがあります。
(gdb) break zif_print_r
Breakpoint 1 at 0x8130d2e: file /home/dqneo/src/php-5.4.14/ext/standard/basic_functions.c, line 5495.
次にPHPコードをワンライナーで実行します。
(gdb) run -r '$x = 123; print_r($x);'
Starting program: /home/dqneo/src/php-5.4.14/sapi/cli/php -r '$x = 123; print_r($x);'
[Thread debugging using libthread_db enabled]
[New Thread 0xb75436c0 (LWP 21957)]
[Switching to Thread 0xb75436c0 (LWP 21957)]

Breakpoint 1, zif_print_r (ht=1, return_value=0xb751ef48, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/dqneo/src/php-5.4.14/ext/standard/basic_functions.c:5495
5495            zend_bool do_return = 0;
ブレークポイントでとまりました。
次に、nextを2回叩いて、引数バリデーションが完了する地点まで駒を進めます。
(gdb) next
5497            if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|b", &var, &do_return) == FAILURE) {
(gdb) next
5501            if (do_return) {
ここで、varという変数がちらっと見えているのに注目してください。
PHPのソースコードを見ると、print_rの引数は内部的には(C言語的には)varという名前の変数であることがわかります。 https://github.com/php/php-src/blob/php-5.4.14/ext/standard/basic_functions.c#L5494

そこで、変数varの中身を見てみます。
(gdb) print var
$1 = (zval *) 0xb753ef88
zval構造体へのポインタであることがわかります。
さらに中身を見てみます。
(gdb) print *var
$2 = {value = {lval = 123, dval = 2.1219958517353468e-314, str = {val = 0x7b <Address 0x7b out of bounds>, len = 1}, ht = 0x7b, obj = {handle = 123, handlers = 0x1}}, refcount__gc = 2, type = 1 '\001',
  is_ref__gc = 0 '\0'}
おお、中身がダンプできましたね!!
おめでとうございます!!
123という数字が見えます。これが、int型の変数の中身です。

value = {...}というのが、後で説明するzvalue_value共用体というやつで、lval = 123がzvalue_value.lvalにあたります。詳しくは後ほど説明します。

お次は文字列のダンプを見てみましょう。
(gdb) run -r '$x = 'abcdef'; print_r($x);'
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/dqneo/src/php-5.4.14/sapi/cli/php -r '$x = 'abcdef'; print_r($x);'
[Thread debugging using libthread_db enabled]
[New Thread 0xb75626c0 (LWP 21965)]
[Switching to Thread 0xb75626c0 (LWP 21965)]
Breakpoint 1, zif_print_r (ht=1, return_value=0xb753df48, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0) at /home/dqneo/src/php-5.4.14/ext/standard/basic_functions.c:5495
5495            zend_bool do_return = 0;
さあブレークポイントで止まりました。
駒を進めます。
(gdb) next
5497            if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|b", &var, &do_return) == FAILURE) {
(gdb) next
5501            if (do_return) {
そして変数varの中身を見てみましょう。
(gdb) print *var
$3 = {value = {lval = -1219235004, dval = 1.4251588407073391e-313, str = {val = 0xb753f344 "abcdef", len = 6}, ht = 0xb753f344, obj = {handle = 3075732292, handlers = 0x6}}, refcount__gc = 2, type = 6 '\006',
  is_ref__gc = 0 '\0'}
やった!!見えた!!
'abcdef'という文字列が見えました!!

ここのvalue = {...}というのがzvalue_value共用体で、str = {...}の部分がzvalue_value.str構造体にあたります。

変数の内部構造について説明するよ

PHPのインタープリタはC言語で書かれており、変数は下記のような構造体として管理されています。
Zend/zend.h
https://github.com/php/php-src/blob/php-5.4.14/Zend/zend.h#L307
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;
PHPの変数の正体は、zval構造体(struct _zval_struct)です。
そして変数の値は、 zvalue_value共用体(union _zvalue_value)になります。

値はzvalue_value共用体が保持している

データ型が数値の場合
$x = 123;
裏側ではzvalue_value共用体のlvalメンバに123という値がセットされます。

C言語のコードで示すと、
zval.value.lval = 123;
のようなイメージです。
データ型が文字列の場合
$y = 'abc';
が実行されたとき、裏側では
zvalue_value共用体のメンバstrのメンバvalに"abc"という値が、
同じくzvalue_value共用体のメンバstrのメンバlenに 3 という値がセットされます。
C言語のコードで示すと、
*(zval.value.str.val) = "abc";
zval.value.str.len = 3;
のようなイメージです。

まとめ

C言語勉強しよう。

参考

カテゴリ: