検索
ホームページphp教程php手册PHP パラメータ受け渡しの原理を深く理解する

PHP パラメータ受け渡しの原理を深く理解する

Jun 16, 2016 am 09:16 AM
php原理パラメータ存在する拡大する深く行く理解する書く質問初め

まず、今日頭に浮かんだ疑問についてお話します。 PHP 拡張機能を作成する場合、パラメーター (つまり、zend_parse_parameters に渡される変数) を自由にする必要はないようです。例:

<span PHP_FUNCTION(test)
{
    </span><span char</span>*<span   str;
    </span><span int</span><span     str_len;

    </span><span if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span "</span><span s</span><span "</span>, &str, &str_len) ==<span  FAILURE) {
        RETURN_FALSE;
    }

    php_printf(str);<br />    <br />    </span><span //</span><span  无需free(str)   </span>
}

正常に動作しています:

test("Hello World"); <span //</span><span  打印Hello World</span>

テスト関数でのメモリ リークを心配する必要はありません。PHP は、パラメータの保存に使用されるこれらの変数を自動的にリサイクルします。

それでは、php はどうやってそれを行うのでしょうか?この問題を説明するには、PHP がパラメータを渡す方法を調べる必要があります。

EG(argument_stack) の概要

簡単に言うと、パラメーターの保存に特に使用されるスタックは、argument_stack という名前で php の EG に保存されます。関数呼び出しが発生するたびに、php は受信パラメータを EG (argument_stack) にプッシュします。関数呼び出しが終了すると、EG (argument_stack) はクリアされ、次の関数呼び出しを待ちます。

EG (argument_stack) の構造体と目的に関して、php5.2 と 5.3 の実装ではいくつかの違いがあります。この記事では主に 5.2 を例に挙げ、5.3 以降の変更点については後ほど説明します。

上の図は 5.2 の argument_stack の大まかな図で、単純明快に見えます。このうちスタックの上下はNULL固定となります。関数によって受け取られたパラメータは、左から右の順序でスタックにプッシュされます。最後に追加の長い値がプッシュされ、スタック内のパラメータの数 (上の図では 10) が示されることに注意してください。

argument_stack にプッシュされるパラメータは何ですか?実際、これらは zval 型のポインタです。それらが指す zva は、CV 変数、is_ref=1 の変数、または定数または定数文字列である可能性があります。

EG (argument_stack) は、php5.2 では zend_ptr_stack タイプとして具体的に実装されています:

typedef <span struct</span><span  _zend_ptr_stack {
    </span><span int</span><span  top;                       // 栈中当前元素的个数
    </span><span int</span><span  max;                       // 栈中最多存放元素的个数
    </span><span void</span> **<span elements;               // 栈底
    </span><span void</span> **<span top_element;            // 栈顶
} zend_ptr_stack;</span>

argument_stack を初期化します

argument_stack の初期化作業は、PHP が特定のリクエストを処理する前に、より正確には PHP インタープリターの起動プロセス中に発生します。

init_executor 関数には次の 2 行があります:

zend_ptr_stack_init(&<span EG(argument_stack));
zend_ptr_stack_push(</span>&EG(argument_stack), (<span void</span> *) NULL);

これらの 2 行は、それぞれ EG (argument_stack) の初期化と、NULL のプッシュを表しています。 EG はグローバル変数であるため、zend_ptr_stack_init が実際に呼び出される前は、EG (argument_stack) 内のすべてのデータはすべて 0 になります。

zend_ptr_stack_init の実装は非常に簡単です。

ZEND_API <span void</span> zend_ptr_stack_init(zend_ptr_stack *<span stack)
{
    stack</span>->top_element = stack->elements = (<span void</span> **) emalloc(<span sizeof</span>(<span void</span> *)*<span PTR_STACK_BLOCK_SIZE);
    stack</span>->max =<span  PTR_STACK_BLOCK_SIZE;   // 栈的大小被初始化成64
    stack</span>->top = <span 0</span><span ;                      // 当前元素个数为0
}</span>

argument_stack が初期化されると、すぐに NULL がプッシュされます。ここで詳しく説明する必要はありませんが、この NULL には実際には意味がありません。

NULL がスタックにプッシュされた後、argument_stack 全体の実際のメモリ配分は次のようになります:

パラメータをスタックにプッシュします

最初の NULL をプッシュした後、別のパラメーターがスタックにプッシュされると、argument_stack で次のアクションが発生します。

stack->top++<span ;
</span>*(stack->top_element++) =<span  参数;</span>

簡単な PHP コードを使用して問題を説明します。

<span function</span> foo( <span $str</span><span  ){
    </span><span print_r</span>(<span 123</span><span );
}
foo(</span>"hello world");

上記のコードは、foo を呼び出すときに文字列定数を渡します。したがって、実際にスタックにプッシュされるのは、ストレージ「hello world」を指す zval です。 vld を使用してコンパイルされたオペコードを表示します:

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   NOP
   6     1      SEND_VAL                                                  OP1[  IS_CONST (458754) 'hello world' ]
         2      DO_FCALL                                      1           OP1[  IS_CONST (458752) 'foo' ]
  15     3    > RETURN                                                    OP1[  IS_CONST (0) 1 ]

SEND_VAL 命令が実際に行うことは、「hello world」を argument_stack にプッシュすることです。

<span int</span><span  ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
        &hellip;&hellip;</span><span 
        zval </span>*<span valptr, </span>*<span value;

        value </span>= &opline-><span op1.u.constant;
        ALLOC_ZVAL(valptr);
        INIT_PZVAL_COPY(valptr, value);
        </span><span if</span> (!<span 0</span><span ) {
            zval_copy_ctor(valptr);
        }<br /><br />        <span // 入栈,<span valptr</span>指向存放hello world的zval</span>
        zend_ptr_stack_push(</span>&<span EG(argument_stack), valptr);  
<span         &hellip;&hellip;</span>
}</span>

プッシュ完了後の argument_stack は次のとおりです:

パラメータの数

前述したように、実際にはすべてのパラメータをスタックにプッシュするだけでは十分ではありません。 PHP は、パラメーターの数を表す追加の数値もプッシュします。この作業は、SEND_XXX 命令中には発生しません。実際、関数を実際に実行する前に、PHP はパラメーターの数をスタックにプッシュします。

引き続き上記の例を使用すると、DO_FCALL 命令を使用して foo 関数を呼び出します。 foo を呼び出す前に、php は argument_stack の最後の部分を自動的に埋めます。

<span static</span> <span int</span><span  zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
    &hellip;&hellip;
    </span><span //</span><span  在argument_stack中压入2个值
    </span><span //</span><span  一个是参数个数(即opline->extended_value)
    </span><span //</span><span  一个是标识栈顶的NULL</span>
    zend_ptr_stack_2_push(&EG(argument_stack), (<span void</span> *)(zend_uintptr_t)opline-><span extended_value, NULL);
    &hellip;&hellip;
    </span><span if</span> (EX(function_state).function->type ==<span  ZEND_INTERNAL_FUNCTION) {
        &hellip;&hellip;
    }
    </span><span else</span> <span if</span> (EX(function_state).function->type ==<span  ZEND_USER_FUNCTION) {
        &hellip;&hellip;
        </span><span //</span><span  调用foo函数</span>
<span         zend_execute(EG(active_op_array) TSRMLS_CC);
    }
    </span><span else</span> { <span /*</span><span  ZEND_OVERLOADED_FUNCTION </span><span */</span><span 
        &hellip;&hellip;
    }
    &hellip;&hellip;
    </span><span //</span><span  清理<span argument_stack</span></span>
<span     zend_ptr_stack_clear_multiple(TSRMLS_C);
    &hellip;&hellip;
    ZEND_VM_NEXT_OPCODE();
}</span>

パラメータの数と NULL をプッシュした後、foo 呼び出しの argument_stack 全体が完了します。

パラメータを取得

引き続き上記の例に従います。 foo 関数を詳しく見て、foo のオペコードがどのようなものかを見てみましょう。

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   3     0  >   RECV                                                      OP1[  IS_CONST (0) 1 ]
   4     1      SEND_VAL                                                  OP1[  IS_CONST (5) 123 ]
         2      DO_FCALL                                      1           OP1[  IS_CONST (459027) 'print_r' ]
   5     3    > RETURN                                                    OP1[  IS_CONST (0) null ]

最初の命令は RECV で、文字通りスタック内のパラメータを取得するために使用されます。実はSEND_VALとRECVはなんとなく対応している気がします。各関数呼び出しの前に SEND_VAL が実行され、関数内で RECV が実行されます。完全に対応しているとは言えませんが、実際には RECV 命令は必ずしも必要ではありません。 RECV は、ユーザー定義関数が呼び出された場合にのみ生成されます。私たちが作成する拡張関数と PHP に付属する組み込み関数には RECV がありません。

各 SEND_VAL と RECV は 1 つのパラメーターのみを処理できることに注意してください。つまり、パラメータの受け渡し処理で複数のパラメータがある場合、複数の SEND_VAL と複数の RECV が生成されます。これは非常に興味深いトピックにつながります。パラメーターの受け渡しとパラメーターの取得の順序は何ですか?

答えは、SEND_VAL がパラメータを左から右にスタックにプッシュするのに対し、RECV は左から右にパラメータを取得するということです。

<span static</span> <span int</span><span  ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    &hellip;&hellip;</span><span //</span><span  param拿参数的顺序是沿着栈顶-->栈底</span>
    <span if</span> (zend_ptr_stack_get_arg(arg_num, (<span void</span> **) &param TSRMLS_CC)==<span FAILURE) {
        &hellip;&hellip;
    } </span><span else</span><span  {
        zend_free_op free_res;
        zval </span>**<span var_ptr;

        </span><span //</span><span  验证参数</span>
        zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *<span param TSRMLS_CC);
        var_ptr </span>= get_zval_ptr_ptr(&opline->result, EX(Ts), &<span free_res, BP_VAR_W);
        
        </span><span //</span><span  获取参数</span>
        <span if</span> (PZVAL_IS_REF(*<span param)) {
            zend_assign_to_variable_reference(var_ptr, param TSRMLS_CC);
        } </span><span else</span><span  {
            zend_receive(var_ptr, </span>*<span param TSRMLS_CC);
        }
    }

    ZEND_VM_NEXT_OPCODE();
}</span>

zend_assign_to_variable_reference と zend_receive はどちらも「パラメータの取得」を完了します。 「パラメータを取得する」ということは、実際には何をするのかを理解するのが簡単ではありません。

最終的には、「パラメーターの取得」は、現在の関数の実行中にこのパラメーターを「シンボル テーブル」に追加することです。これは、具体的には EG (current_execute_data)->symbol_table に対応します。この例では、RECV が完了した後、関数本体のsymbol_table にシンボル 'str' があり、その値は "hello world" です。

但argument_stack并没有发生一丝变化,因为RECV仅仅是读取参数,而不会对栈产生类似pop操作。

清理argument_stack

foo内部的print_r也是一个函数调用,因此也会产生压栈-->清栈的操作。因此print_r执行之前的argument_stack为:

print_r执行之后argument_stack又回到了foo刚RECV完的状态。

具体调用print_r的过程并非本文阐述的重点。我们关心的是当调用foo结束之后,php是如何清理argument_stack的。

上面展示的do_fcall代码片段中可以看出,清理工作由

<span static</span> inline <span void</span><span  zend_ptr_stack_clear_multiple(TSRMLS_D)
{
    </span><span void</span> **p = EG(argument_stack).top_element-<span 2</span><span ;
    </span><span //</span><span  取栈顶保存的参数个数</span>
    <span int</span> delete_count = (<span int</span>)(zend_uintptr_t) *<span p; 
    EG(argument_stack).top </span>-= (delete_count+<span 2</span><span );
    
    </span><span //</span><span  从上至下,依次清理</span>
    <span while</span> (--delete_count>=<span 0</span><span ) {
        zval </span>*q = *(zval **)(--<span p);
        </span>*p =<span  NULL;
        zval_ptr_dtor(</span>&<span q);
    }
    EG(argument_stack).top_element </span>=<span  p;
}</span>

注意这里清理栈中zval指针,使用的是zval_ptr_dtor。zval_ptr_dtor会将refcount减1,一旦refcount减为0,则保存该变量的内存区域会被真正的回收掉。

在本文示例中,foo调用完毕之后,保存“hello world”的zval状态为:

value        "hello world"
refcount     1
type         6
is_ref       0

由于refcount只剩1,因此,zval_ptr_dtor会将“hello world”真正从内存中销毁。

消栈完毕之后的argument_stack内存状态为:

可以看出上图中的argument_stack与刚被初始化之后是一样的。此时argument_stack真正做好了迎接下一次函数调用的准备。

回到文章刚开始的问题...

为何无需free(str)呢?弄明白了argument_stack之后就很好理解这个问题了。

因为str指向的是zval中实际存放“hello world”的内存地址。假设扩展函数如下:

<span PHP_FUNCTION(test)
{
    </span><span char</span>*<span   str;
    </span><span int</span><span     str_len;

    </span><span if</span> (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, <span "</span><span s</span><span "</span>, &str, &str_len) ==<span  FAILURE) {
        RETURN_FALSE;
    }

    str[</span><span 0</span>] = <span '</span><span H</span><span '</span><span ;
}</span>

则调用

$a = <span "</span><span hello world</span><span "</span><span ;
test($a);
echo $a;</span>

会输出“Hello world”。尽管我们调用test的时候,并非是传$a的引用,但实际效果相当于test(&$a)。

简单来说,内存中只有一份$a,不管是CV数组中,还是在argument_stack中。而zend_parse_parameters并没有拷贝一份数据用于函数执行,事实上它也不能这么做。因此,当函数完毕之后,如果没有其他地方会用到$a,php清理argument_stack时会帮我们free。如果仍然其他代码在使用,就更加不能手动free了,否则会破坏$a的内存区域。

需要注意的是,并非写扩展函数中用到的每个变量,php都会自动回收。所以该free的时候,切勿手软:)

 

声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

mPDF

mPDF

mPDF は、UTF-8 でエンコードされた HTML から PDF ファイルを生成できる PHP ライブラリです。オリジナルの作者である Ian Back は、Web サイトから「オンザフライ」で PDF ファイルを出力し、さまざまな言語を処理するために mPDF を作成しました。 HTML2FPDF などのオリジナルのスクリプトよりも遅く、Unicode フォントを使用すると生成されるファイルが大きくなりますが、CSS スタイルなどをサポートし、多くの機能強化が施されています。 RTL (アラビア語とヘブライ語) や CJK (中国語、日本語、韓国語) を含むほぼすべての言語をサポートします。ネストされたブロックレベル要素 (P、DIV など) をサポートします。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Eclipse を SAP NetWeaver アプリケーション サーバーと統合します。

WebStorm Mac版

WebStorm Mac版

便利なJavaScript開発ツール

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

このプロジェクトは osdn.net/projects/mingw に移行中です。引き続きそこでフォローしていただけます。 MinGW: GNU Compiler Collection (GCC) のネイティブ Windows ポートであり、ネイティブ Windows アプリケーションを構築するための自由に配布可能なインポート ライブラリとヘッダー ファイルであり、C99 機能をサポートする MSVC ランタイムの拡張機能が含まれています。すべての MinGW ソフトウェアは 64 ビット Windows プラットフォームで実行できます。

VSCode Windows 64 ビットのダウンロード

VSCode Windows 64 ビットのダウンロード

Microsoft によって発売された無料で強力な IDE エディター