web-dev-qa-db-ja.com

可変引数はgccでどのように実装されていますか?

int max(int n, ...)

私はcdecl呼び出し規約を使用しており、呼び出し先が戻った後に呼び出し元が変数をクリーンアップします。

マクロがどのように行われるのか知りたいですva_endva_startおよびva_arg仕事?

呼び出し元は、引数の配列のアドレスをmaxの2番目の引数として渡しますか?

24
Bruce

C言語がパラメーターをスタックに格納する方法を見ると、マクロが機能する方法が明確になるはずです。-

Higher memory address    Last parameter
                         Penultimate parameter
                         ....
                         Second parameter
Lower memory address     First parameter
       StackPointer  ->  Return address

(ハードウェアによっては、スタックポインタが1行下になり、上位と下位が入れ替わる場合があることに注意してください)

引数は常にこのように格納されます1...パラメータタイプがなくても。

va_startマクロは、最初の関数パラメーターへのポインターを設定するだけです。例:-

 void func (int a, ...)
 { 
   // va_start
   char *p = (char *) &a + sizeof a;
 }

これにより、pが2番目のパラメーターを指すようになります。 va_argマクロはこれを行います:-

 void func (int a, ...)
 { 
   // va_start
   char *p = (char *) &a + sizeof a;

   // va_arg
   int i1 = *((int *)p);
   p += sizeof (int);

   // va_arg
   int i2 = *((int *)p);
   p += sizeof (int);

   // va_arg
   long i2 = *((long *)p);
   p += sizeof (long);
 }

va_endマクロは、p値をNULLに設定するだけです。

ノート:

  1. 最適化コンパイラと一部のRISCCPUは、スタックを使用するのではなく、パラメータをレジスタに格納します。 ...パラメーターが存在すると、この機能がオフになり、コンパイラーがスタックを使用できるようになります。
30
Skizz

引数がスタックに渡されると、_va__「関数」(ほとんどの場合、マクロとして実装されます)は、プライベートスタックポインターを操作するだけです。このプライベートスタックポインタは、_va_start_に渡された引数から格納され、_va_arg_は、パラメータを反復するときに「スタック」から引数を「ポップ」します。

次のように、3つのパラメーターを使用して関数maxを呼び出したとします。

_max(a, b, c);
_

max関数内では、スタックは基本的に次のようになります。

 + ----- + 
 | c | 
 | b | 
 | a | 
 | ret | 
 SP-> + ----- + 

SPは実際のスタックポインタであり、実際にはab、およびcではなく、それらの値です。 retは、関数が実行されたときにジャンプするリターンアドレスです。

va_start(ap, n)は、引数のアドレス(関数プロトタイプではn)を取得し、そこから次の引数の位置を計算するため、新しいプライベートスタックポインターを取得します。

 + ----- + 
 | c | 
 ap-> | b | 
 | a | 
 | ret | 
 SP-> + ----- + 

va_arg(ap, int)を使用すると、プライベートスタックポインタが指すものを返し、プライベートスタックポインタを次の引数を指すように変更することで「ポップ」します。スタックは次のようになります。

 + ----- + 
 ap-> | c | 
 | b | 
 | a | 
 | ret | 
 SP-> + ----- + 

この説明はもちろん簡略化されていますが、原理を示しています。

一般的に、関数プロトタイプが(、...)で宣言されている場合、target.defをどのように取得するか、コンパイラはvarargsフラグと名前付き引数の型への参照でマークされた解析ツリーを設定します。厳密なC準拠の場合、各名前付き引数は、そのパラメーターがva_startの名前付きフィールドであり、va_arg()に戻る可能性がある場合に、va_listをセットアップするために必要な追加情報を取得する必要がありますが、ほとんどのコンパイラーは、最後の名前付き引数に対してこの情報を生成するだけです。 。関数が定義されると、プロローグジェネレーターはvarargsフラグが設定されたことを記録し、va_startマクロが参照できる既知のオフセットを持つフレームに追加する非表示フィールドを設定するために必要なコードを追加します。

その関数への参照が見つかると、...を表す引数ごとに追加の解析ツリーとコード生成ツリーが作成されます。これにより、va_startの設定フィールドに追加される、配列境界などの実行時型情報の追加の非表示フィールドが導入される場合があります。名前付き引数の場合はva_arg。この結合されたツリーは、パラメーター値をフレームにコピーするために生成されるコードを決定し、プロローグは、va_startが任意または最後の名前のパラメーターで始まるva_listを作成するために必要なものを設定し、va_arg()を呼び出すたびに参照するインラインコードを生成しますコンパイル時に期待される戻り値を検証するために使用されるパラメーター固有の非表示フィールドは、コンパイルされる式の使用法と互換性のある割り当てであり、必要な引数の昇格/強制を実行します。名前付きフィールドの値のサイズと非表示のフィールドのサイズの合計によって、呼び出し後にコンパイルされる値、または呼び出し先クリーンアップモデルの関数エピローグで、戻り時にフレームを調整する値が決まります。

これらの各ステップには、config/proc/proc.cファイルとproc.hファイルにカプセル化されたプロセッサと呼び出し規約の依存関係があり、各引数に固定サイズが割り当てられていると想定するva_start()とva_arg()の単純なデフォルト定義をオーバーライドします。スタック上の最初の名前付き引数の少し上の距離。一部のプラットフォームまたは言語では、固定サイズのスタックよりも、個別のmalloc()として実装されたパラメーターフレームの方が望ましいです。また、これらの使用法はスレッドセーフではないことに注意してください。関数の戻りやスレッドの中止によってパラメータフレームが無効にならないようにするための不特定の手段なしに、va_list参照を別のスレッドに渡すことは安全ではありません。

0
M. Ziegast