int max(int n, ...)
私はcdecl
呼び出し規約を使用しており、呼び出し先が戻った後に呼び出し元が変数をクリーンアップします。
マクロがどのように行われるのか知りたいですva_end
、va_start
およびva_arg
仕事?
呼び出し元は、引数の配列のアドレスをmaxの2番目の引数として渡しますか?
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
に設定するだけです。
ノート:
...
パラメーターが存在すると、この機能がオフになり、コンパイラーがスタックを使用できるようになります。引数がスタックに渡されると、_va_
_「関数」(ほとんどの場合、マクロとして実装されます)は、プライベートスタックポインターを操作するだけです。このプライベートスタックポインタは、_va_start
_に渡された引数から格納され、_va_arg
_は、パラメータを反復するときに「スタック」から引数を「ポップ」します。
次のように、3つのパラメーターを使用して関数max
を呼び出したとします。
_max(a, b, c);
_
max
関数内では、スタックは基本的に次のようになります。
+ ----- + | c | | b | | a | | ret | SP-> + ----- +
SP
は実際のスタックポインタであり、実際にはa
、b
、および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参照を別のスレッドに渡すことは安全ではありません。