x64 でのスタックの使用

RSP の現在のアドレスを超えるメモリはすべて揮発性と見なされます。OS またはデバッガーは、ユーザー デバッグ セッションまたは割り込みハンドラー中にこのメモリを上書きする可能性があります。 したがって、常に、スタック フレームの値を読み書きする前に、RSP を設定する必要があります。

ここでは、ローカル変数に対するスタック領域の割り当てと、alloca 組み込み関数について説明します。

スタックの割り当て

関数のプロローグでは、ローカル変数、保存されたレジスタ、スタック パラメーター、レジスタ パラメーターに対するスタック領域を割り当てる必要があります。

パラメーター領域は、すべての関数呼び出しで常に戻りアドレスに隣接するように、常にスタックの一番下にあります (alloca が使用されている場合でも)。 そこには、少なくとも 4 つのエントリが含まれますが、呼び出される可能性のある関数で必要なすべてのパラメーターを保持するのに十分な領域が常に含まれます。 パラメーター自体がスタックに属さない場合でも、レジスタ パラメーターに対して常に領域が割り当てられることに注意してください。呼び出し先に対しては、すべてのパラメーターの領域が割り当てられていることが保証されます。 レジスタ引数にはホーム アドレスが必要であるため、呼び出された関数で引数リスト (va_list) または個別の引数のアドレスを取得する必要がある場合は、連続する領域を使用できます。 また、この領域は、サンク実行時およびデバッグ オプションとして、レジスタ引数を保存するための便利な場所にもなります (たとえば、プロローグ コードのホーム アドレスに格納されていれば、デバッグ時に引数を見つけやすくなります)。 呼び出された関数のパラメーターが 4 つ未満の場合でも、これら 4 つのスタックの場所は、呼び出された関数によって実質的に所有され、呼び出された関数で、パラメーター レジスタ値保存以外の他の目的に使用できます。 したがって、呼び出し元では、関数呼び出しの期間を通じて、スタックのこの領域に情報を保存することはできません。

関数で領域が動的に割り当てられる場合 (alloca)、スタックの固定部分のベースをマークするために、不揮発性レジスタをフレーム ポインターとして使用する必要があり、そのレジスタをプロローグで保存および初期化する必要があります。 alloca を使用すると、同じ呼び出し元から同じ呼び出し先への複数の呼び出しで、レジスタ パラメーターのホーム アドレスが異なる場合があることに注意してください。

スタックは常に 16 バイトでアラインされて維持されます。ただし、プロローグ内 (たとえば、戻りアドレスがプッシュされた後) と、特定のクラスのフレーム関数に対して関数型で指定されている場合を除きます。

次に示すのは、関数 A で非リーフ関数 B を呼び出す場合のスタック レイアウトの例です。関数 A のプロローグにおいて、スタックの一番下に、B で必要なすべてのレジスタ パラメーターとスタック パラメーターのための領域が、既に割り当てられています。 呼び出しによって戻りアドレスがプッシュされ、B のプロローグによって、そのローカル変数の領域、不揮発性レジスタ、および関数の呼び出しに必要な領域が割り当てられます。 B で alloca が使用されている場合は、ローカル変数および不揮発性レジスタの保存領域と、パラメーター スタック領域の間に、領域が割り当てられます。

Diagram of the stack layout for the x64 conversion example.

関数 B で別の関数が呼び出されると、RCX のホーム アドレスのすぐ下に、戻りアドレスがプッシュされます。

動的なパラメーター スタック領域の構成

フレーム ポインターが使用されている場合は、パラメーター スタック領域を動的に作成するオプションがあります。 現在、これは x64 コンパイラでは行われません。

関数型

関数には基本的に 2 つの種類があります。 スタック フレームを必要とする関数は、"フレーム関数" と呼ばれます。 スタック フレームを必要としない関数は、"リーフ関数" と呼ばれます。

フレーム関数は、スタック領域を割り当て、他の関数を呼び出し、不揮発性レジスタを保存して、例外処理を使用する関数です。 また、関数テーブルのエントリも必要です。 フレーム関数には、プロローグとエピローグが必要です。 フレーム関数では、スタック領域を動的に割り当てたり、フレーム ポインターを使用したりすることができます。 フレーム関数では、この呼び出し標準の完全な機能を自由に使用できます。

フレーム関数で別の関数を呼び出さない場合は、スタックをアラインする必要はありません (「スタックの割り当て」を参照)。

リーフ関数は、関数テーブルのエントリを必要としない関数です。 RSP などのすべての不揮発性レジスタを変更することはできません。これは、関数を呼び出したり、スタック領域を割り当てたりすることができないことを意味します。 実行中にスタックをアラインされていない状態のままにすることができます。

malloc のアラインメント

malloc では、オブジェクトが基本的なアラインメントに従い、割り当てられたメモリ量の限度で格納できる限り、どのようなオブジェクトを格納する場合でも適切なアラインメントのメモリを返すことが保証されます。 "基本的なアラインメント" とは、アラインメントを指定せずに実装によってサポートされる最大のアラインメントを上限とするアラインメントです。 (Visual C++ では、これは、8 バイトに必要な doubleアラインメントです。64 ビット プラットフォームを対象とするコードでは、16 バイトです)。たとえば、4 バイト割り当ては、4 バイト以下のオブジェクトをサポートする境界に配置されます。

Visual C++ では、"拡張アラインメント" を持つ型が許可されています。これは別名、"オーバーアラインメント" 型とも呼ばれます。 たとえば、SSE 型の __m128__m256、また __declspec(align( n )) で、n に 8 を超える数値を設定して宣言した型などです。 オブジェクトで拡張アラインメントが必要な場合、そのオブジェクトに適した境界上でのメモリのアラインメントは、malloc によって保証されません。 オーバーアラインメント型にメモリを割り当てるには、_aligned_malloc とその関連の関数を使用します。

alloca

_alloca は、16 バイトでアラインされる必要があり、さらにフレーム ポインターを使用する必要があります。

スタックの割り当て」で説明されているように、割り当てられたスタックの後には、後で呼び出される関数のパラメーターのための領域が含まれる必要があります。

関連項目

x64 ソフトウェア規約
align
__declspec