おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

func_get_args系の関数の変な動きから、EG(argument_stack)を中途半端に眺める (続き)

(func_get_args系の関数とは、func_get_args / func_get_arg / func_num_args の3つです。)
前回の続き。
ちょっと間違えてた。くまさんから指摘。
関数の引数パラメータとしてfunc_get_args()を使っても、引数の1個目であればきちんと動く。

<?php
function hoge()
{
  var_dump(join(',', func_get_args()));
}
hoge(1, 2, 3); 
?>

は、"PHP Fatal error: func_get_args(): Can't be used as a function parameter" とエラーが出て動かないわけなのですが、

<?php
function hoge()
{
  var_dump(join(func_get_args(), ','));
}
hoge(1, 2, 3); 
?>

は動く。すなわち、関数の引数としてfunc_get_args()を使うとき、第1引数ならOKということ。
join(',', func_get_args()) だと、スタックに ',' が積まれるが、join(func_get_args(), ',') だと、先頭の引数だから積まれていないから。そして、join (というか、implode)は、歴史的な理由により、引数をどちらの順番でも受けつけることが可能であるから。バッドノウハウすぎるのでやっちゃだめですが、こんなの。
結局、argument_stackに想定外に載っていると動かないのですが、関数の先頭の引数だと自分の前には積む人がいないからOKということ。引数の処理に入るときに、なにかpaddingでも詰めるのかと思ったら、そういうことはしていないようで。

どちらにしろdebug_backtrace()は正しく動く

debug_backtraceの例も悪かったので、新しいのを。
implode()だと、多次元配列を食わせられないので、var_dump()にしています。var_dumpは引数を複数指定することもできて、その場合は、1個ずつvar_dumpを呼び出すのと同じ結果が得られます。
まずは、func_get_args()の場合。

<?php
function hoge()
{
  var_dump(func_get_args(), 2);
  var_dump(1, func_get_args());
}
hoge(1, 2, 3);
?>

これは以下の結果を返します。

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}
int(2)
PHP Fatal error:  func_get_args(): Can't be used as a function parameter

上のvar_dumpが成功して、下のvar_dumpで失敗していることがわかります。
これを、debug_backtraceに置き換えた以下のコードでは、

<?php
function hoge()
{
  var_dump(debug_backtrace(), 2); 
  var_dump(1, debug_backtrace());
}
hoge(1, 2, 3); 
?>

普通に動いて、

array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(23) "/home/iogi/tmp/test.php"
    ["line"]=>
    int(7)
    ["function"]=>
    string(4) "hoge"
    ["args"]=>
    array(3) {
      [0]=>
      &int(1)
      [1]=>
      &int(2)
      [2]=>
      &int(3)
    }
  }
}
int(2)
int(1)
array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(23) "/home/iogi/tmp/test.php"
    ["line"]=>
    int(7)
    ["function"]=>
    string(4) "hoge"
    ["args"]=>
    array(3) {
      [0]=>
      &int(1)
      [1]=>
      &int(2)
      [2]=>
      &int(3)
    }
  }
}

となる。正しく引数は取れている。

vld (Vulcan Logic Disassembler)を使ってみよう

そして、くまさんが vld で動きを確認していた。これでみると、引数スタックの可視化はできないものの、若干ZendEngineの動きがわかりやすくなります。

vldとは

The Vulcan Logic Disassembler hooks into the Zend Engine and dumps all the opcodes (execution units) of a script. It was written as as a beginning of an encoder, but I never got the time for that. It can be used to see what is going on in the Zend Engine.

Projects — Derick Rethans

ということで、PHPコードがどのようにZend Engineレベルで動作するかを見ることができるディスアセンブラです。

インストール

peclからインストールするとうまくいかなかったので*1、小泉さんの記事を参考にCVSからcheckoutしてきて、ビルド、インストールします。PHP5.2でも動作しました。

簡単な実行例

まずは、どういうものかを理解するために、シンプルなもので見てみたいと思います。

1 <?php
2 $a = "this is";
3 $b = $a;
4 $c = $a;
5 $c = 42;
6 unset($b);
7 unset($c);
8 ?>

これを実行すると、

$ php -dvld.active=1 test.php

このような結果が得られます。

Branch analysis from position: 0
Return found
function name:  (null)
number of ops:  8
compiled vars:  !0 = $a, !1 = $b, !2 = $c
line     #  op                           fetch          ext  return  operands
-------------------------------------------------------------------------------
   2     0  ASSIGN                                                   !0, 'this+is'
   3     1  ASSIGN                                                   !1, !0
   4     2  ASSIGN                                                   !2, !0
   5     3  ASSIGN                                                   !2, 42
   6     4  UNSET_VAR                                                'b'
   7     5  UNSET_VAR                                                'c'
   9     6  RETURN                                                   1
         7* ZEND_HANDLE_EXCEPTION                   

読み方は、例えば、ASSIGN !0, 'this+is'で、!0 は compiled vars のところに書いてあるように $a なので、$a に 'this+is' を割り当て、となります。(なぜ this+is とスペースが+に変換されるのかは不明)
ここに示しているコードは、先日シカゴで開催された php|tek での Derick Rethansのプレゼン資料、PHP Secrets (PDF)の付録、27ページの図に対応してます。参考までに。

vldでfunc_get_argsの場合を見てみる

まずは、成功パターン
1 <?php
2 function hoge()
3 {
4     echo var_dump(func_get_args("b"), "c", "d");
5 }
6 hoge("a");
7 ?>

分かりやすくなるようにわざとfunc_get_argsに不要な引数 "b" を渡しています。
この場合は、以下のような結果が得られます。

Branch analysis from position: 0
Return found
filename:       /home/iogi/tmp/test.php
function name:  (null)
number of ops:  5
compiled vars:  none
line     #  op                           fetch          ext  return  operands
-------------------------------------------------------------------------------
   2     0  NOP                                                      
   6     1  SEND_VAL                                                 'a'
         2  DO_FCALL                                      1          'hoge'
   8     3  RETURN                                                   1
         4* ZEND_HANDLE_EXCEPTION                                    

Function hoge:
Branch analysis from position: 0
Return found
filename:       /home/iogi/tmp/test.php
function name:  hoge
number of ops:  9
compiled vars:  none
line     #  op                           fetch          ext  return  operands
-------------------------------------------------------------------------------
   4     0  SEND_VAL                                                 'b'
         1  DO_FCALL                                      1          'func_get_args'
         2  SEND_VAR_NO_REF                                          $0
         3  SEND_VAL                                                 'c'
         4  SEND_VAL                                                 'd'
         5  DO_FCALL                                      3          'var_dump'
         6  ECHO                                                     $1
   5     7  RETURN                                                   null
         8* ZEND_HANDLE_EXCEPTION                                    

End of function hoge.

ユーザ関数を呼び出すと、それは別に記述されるので、このように2つ出てきます。上のものについては、'a' を積んで(#1) hoge という関数を呼んでいる(#2)ことが分かるとおもいます。
そして、関数hoge()側では、まず "b" を詰めて(#0)、func_get_args を呼ぶ(#1)。関数に入ってからこの時点までは、自身の引数(b)以外にスタックには何も積まれてないのでOK。

失敗パターン

こんどは、func_get_argsより前に引数 "x", "y" をもってきました。

1 <?php
2 function hoge()
3 {
4     echo var_dump("x", "y", func_get_args("b"));
5 }
6 hoge("a");
7 ?>

で、こうなる。

line     #  op                           fetch          ext  return  operands
-------------------------------------------------------------------------------
   4     0  SEND_VAL                                                 'x'
         1  SEND_VAL                                                 'y'
         2  SEND_VAL                                                 'b'
         3  DO_FCALL                                      1          'func_get_args'
         4  SEND_VAR_NO_REF                                          $0
         5  DO_FCALL                                      3          'var_dump'
         6  ECHO                                                     $1
   5     7  RETURN                                                   null
         8* ZEND_HANDLE_EXCEPTION                                    

引数の "x", "y" が先にセットされて(#0, #1)、その後にfunc_get_argsの引数詰め(#2)、関数呼び出し(#3)が行われています。これだと、スタックに何か積まれていてNG。

まとめ

  • やっぱり、func_get_argsの実装はいけてない。
    • debug_backtraceのようにちゃんと書かないのは、理由があるのだろうか。
  • vldはおもしろいけど、普段役に立つことはない
  • Opcodeの話なんてのも書いてるAdvanced Php Programming本は素敵。

Advanced PHP Programming: Developing Large-Scale Web Applications with PHP 5 (2nd Edition) (Developer's Library)

Advanced PHP Programming: Developing Large-Scale Web Applications with PHP 5 (2nd Edition) (Developer's Library)

*1:手元の環境では、OSXのPHP4.4.4でビルドはできたものの、Symbol Undefinedエラー、PHP5.1/5.2 on UbuntuはZEND_JMP_NO_CTOR undeclaredエラー