ARM64 ABI 規則の概要
Windows 用の基本的なアプリケーション バイナリ インターフェイス (ABI) を、ARM プロセッサ上で 64 ビット モード (ARMv8 以降のアーキテクチャ) でコンパイルして実行するとき、ほとんどの部分は ARM の標準 AArch64 EABI に従います。 この記事では、EABI の主な前提条件と、そこに記載されている内容からの変更点について説明します。 32 ビット ABI の詳細については、「ARM ABI 規則の概要」を参照してください。 標準 ARM EABI の詳細については、「ARM アーキテクチャ用のアプリケーション バイナリ インターフェイス (ABI)」(外部リンク) を参照してください。
定義
64 ビット サポートの導入に伴い、ARM ではいくつかの用語が定義されています。
- AArch32 - Thumb モードの実行を含む、ARM によって定義されたレガシ 32 ビット命令セット アーキテクチャ (ISA)。
- AArch64 - ARM によって定義された新しい 64 ビット命令セット アーキテクチャ (ISA)。
- ARMv7 - AArch32 のサポートのみを含む "7 世代" ARM ハードウェアの仕様。 このバージョンの ARM ハードウェアは、サポートされる ARM 版 Windows の最初のバージョンです。
- ARMv8 - AArch32 と AArch64 のサポートの両方を含む "8 世代" ARM ハードウェアの仕様。
Windows では、次の用語も使用されます。
- ARM - 32 ビット ARM アーキテクチャ (AArch32) のことを指します。WoA (Windows on ARM) と呼ばれることもあります。
- ARM32 - 上記の ARM と同じです。このドキュメントでは、わかりやすくするために使用されています。
- ARM64 - 64 ビット ARM アーキテクチャ (AArch64) のことを指します。 WoA64 というものはありません。
最後に、データ型に関しては、ARM からの次の定義が参照されています。
- ショートベクター - SIMD で直接表現可能なデータ型。8 バイトまたは 16 バイト相当の要素のベクターです。 8 バイトまたは 16 バイトのいずれかのサイズに合わせてアラインされます。各要素は 1、2、4、または 8 バイトです。
- HFA (同種浮動小数点集計) - 2 個から 4 個の同一の浮動小数点メンバー (float または double) を持つデータ型。
- HVA (同種ショートベクター集計) - 2 個から 4 個の同一のショートベクター メンバーを持つデータ型。
基本要件
ARM64 版 Windows は、ARMv8 以降のアーキテクチャで常に実行されることを前提とします。 浮動小数点と NEON の両方のサポートがハードウェアに存在するものと見なされます。
ARMv8 仕様には、AArch32 と AArch64 の両方に対して、新しいオプションの crypto および CRC ヘルパー オペコードが記述されています。 現在、これらに対するサポートはオプションですが、推奨されます。 これらのオペコードを利用するには、まずアプリでランタイム チェックを実行してそれらの有無を確認する必要があります。
エンディアン
ARM32 版 Windows と同様に、ARM64 はリトルエンディアン モードで実行されます。 AArch64 ではカーネル モードのサポートなしではエンディアンの切り替えは困難であるため、より簡単に適用できます。
Alignment
ARM64 で実行されている Windows では、CPU ハードウェアでミスアライン アクセスを透過的に処理できます。 AArch32 からの改善により、このサポートはすべての整数アクセス (マルチワード アクセスを含む) と浮動小数点数アクセスで機能するようになりました。
ただし、キャッシュされていない (デバイス) メモリへのアクセスは引き続き、常にアラインされている必要があります。 コードでキャッシュされていないメモリからミスアライン データの読み取りまたは書き込みが行われる可能性がある場合は、すべてのアクセスをアラインすることを確認する必要があります。
ローカル用のレイアウトの既定のアラインメント:
サイズ (バイト単位) | アラインメント (バイト単位) |
---|---|
1 | 1 |
2 | 2 |
3、4 | 4 |
> 4 | 8 |
グローバルおよび静的なレイアウトの既定のアラインメント:
サイズ (バイト単位) | アラインメント (バイト単位) |
---|---|
1 | 1 |
2 - 7 | 4 |
8 - 63 | 8 |
>= 64 | 16 |
整数レジスタ
AArch64 アーキテクチャでは、32 個の整数レジスタがサポートされています。
Register | 揮発度 | ロール |
---|---|---|
x0-x8 | Volatile | パラメーター/結果スクラッチ レジスタ |
x9-x15 | Volatile | スクラッチ レジスタ |
x16-x17 | Volatile | プロシージャ内呼び出しスクラッチ レジスタ |
x18 | 該当なし | 予約プラットフォーム レジスタ: カーネル モードでは、現在のプロセッサの KPCR を指します。ユーザー モードでは、TEB を指す |
x19-x28 | 非 volatile | スクラッチ レジスタ |
x29/fp | 非 volatile | フレーム ポインター |
x30/lr | 両方 | リンク レジスタ: 呼び出し先関数は、それ自体の戻り値を保持する必要がありますが、呼び出し元の値は失われます。 |
各レジスタには、完全な 64 ビット値 (x0-x30 を使用) または 32 ビット値 (w0-w30 を使用) としてアクセスできます。 32 ビット演算では、結果を最大で 64 ビットまでゼロ拡張できます。
パラメーター レジスタの使用方法の詳細については、「パラメーター渡し」のセクションを参照してください。
AArch32 とは異なり、プログラム カウンター (PC) とスタック ポインター (SP) はインデックス付きレジスタではありません。 アクセス方法は制限されています。 また、x31 レジスタがないことにも注目してください。 このエンコードは、特別な目的で使用されます。
フレーム ポインター (x29) は、ETW などのサービスで使用される高速スタック ウォークとの互換性を確保するために必要です。 スタック上の前の {x29, x30} ペアをポイントする必要があります。
浮動小数点/SIMD レジスタ
AArch64 アーキテクチャでは、次に示すように、32 個の浮動小数点/SIMD レジスタもサポートされています。
Register | 揮発度 | ロール |
---|---|---|
v0-v7 | Volatile | パラメーター/結果スクラッチ レジスタ |
v8-v15 | 両方 | 下位 64 ビットは不揮発性です。 上位 64 ビットは揮発性です。 |
v16-v31 | Volatile | スクラッチ レジスタ |
各レジスタには、(v0-v31 または q0-q31 を介して) 完全な 128 ビット値としてアクセスできます。 これには、(d0-d31 を介して) 64 ビット値として、(s0-s31 を介して) 32 ビット値として、(h0-h31 を介して) 16 ビット値として、または (b0-b31 を介して) 8 ビット値としてアクセスできます。 128 ビットより小さいアクセスは、完全な 128 ビット レジスタの下位ビットにのみアクセスします。 特に指定がない限り、残りのビットはそのまま残ります (AArch64 は、より小さいレジスタがより大きなレジスタの上にパックされていた AArch32 とは異なります)。
浮動小数点制御レジスタ (FPCR) には、その中にあるさまざまなビット フィールドに関する次の要件があります。
Bits | 意味 | 揮発度 | ロール |
---|---|---|---|
26 | AHP | 非揮発性 | 代替の半精度制御。 |
25 | DN | 非揮発性 | 既定の NaN モード制御。 |
24 | FZ | 非 volatile | Flush-to-zero モード制御。 |
23-22 | RMode | 非 volatile | 丸めモード制御。 |
15、12-8 | IDE/IXE/その他 | 非揮発性 | 例外トラップ イネーブル ビット。常に 0 である必要があります。 |
システム レジスタ
AArch32 と同様に、AArch64 仕様には次のシステム制御の "スレッド ID" レジスタが 3 つ用意されています。
Register | ロール |
---|---|
TPIDR_EL0 | 予約済み。 |
TPIDRRO_EL0 | 現在のプロセッサの CPU 数を格納します。 |
TPIDR_EL1 | 現在のプロセッサの KPCR 構造体をポイントします。 |
浮動小数点例外
AArch64 システムでは、IEEE 浮動小数点例外のサポートはオプションです。 ハードウェア浮動小数点例外をサポートするプロセッサ バリアントでは、Windows カーネルが例外をサイレントにキャッチし、FPCR レジスタで暗黙的に無効にします。 このトラップにより、プロセッサ バリアント全体で動作が正常に保たれます。 そうでない場合、例外をサポートしていないプラットフォームで開発されたコードは、サポートがあるプラットフォームで実行しているときに、予期しない例外が発生する可能性があります。
パラメーター渡し
非可変個引数関数の場合、Windows ABI はパラメーター渡しに関して ARM によって指定された規則に従います。 これらの規則は、AArch64 アーキテクチャのプロシージャ呼び出し標準から直接抜粋されます。
ステージ A - 初期化
このステージは、引数の処理が開始される前に 1 回だけ実行されます。
次の汎用レジスタ番号 (NGRN) は 0 に設定されます。
次の SIMD および浮動小数点レジスタ番号 (NSRN) は 0 に設定されます。
次のスタック引数アドレス (NSAA) は現在のスタックポインター値 (SP) に設定されます。
ステージ B - 引数のプレパディングおよび拡張
リストの各引数には、次のリストで最初に一致する規則が適用されます。 一致するルールがない場合、引数は変更されずに使用されます。
引数の型が呼び出し元や呼び出し先からサイズを静的に決定できない複合型である場合、引数はメモリにコピーされ、その引数がそのコピーへのポインターによって置き換えられます (C/C++ にはそのような型はありませんが、他の言語または言語拡張機能には存在します)。
引数の型が HFA または HVA の場合、引数は変更されずに使用されます。
引数の型が 16 バイトより大きい複合型である場合、引数は呼び出し元によって割り当てられたメモリにコピーされ、その引数がそのコピーへのポインターによって置き換えられます。
引数の型が複合型の場合、引数のサイズが最も近い 8 の倍数のバイト数に丸められます。
ステージ C - レジスタおよびスタックへの引数の割り当て
リストの各引数には、引数が割り当てられるまで次の規則が順番に適用されます。 レジスタに引数が割り当てられていると、レジスタ内の未使用のビットの値はすべて未指定になります。 引数がスタック スロットに割り当てられている場合、未使用のパディングのバイト数の値はすべて未指定になります。
引数が半精度、単精度、倍精度、四倍精度浮動小数点型、またはショート ベクター型で、NSRN が 8 未満の場合、引数はレジスタ v[NSRN] の最下位ビットに割り当てられます。 NSRN は 1 ずつインクリメントされます。 これで引数が割り当てられました。
引数が HFA または HVA であり、未割り当ての SIMD および浮動小数点レジスタが十分にある場合 (NSRN + メンバーの数が 8 以下)、引数は SIMD および浮動小数点レジスタ (HFA または HVA のメンバーごとに 1 つのレジスタ) に割り当てられます。 NSRN は使用したレジスタの数だけインクリメントされます。 これで引数が割り当てられました。
引数が HFA または HVA である場合、NSRN は 8 に設定され、引数のサイズが最も近い 8 の倍数のバイト数に丸められます。
引数が HFA、HVA、四倍精度浮動小数点型、またはショート ベクター型の場合、NSAA は 8 より大きい値またはその引数の型の自然なアラインメントに切り上げられます。
引数が半精度または単精度浮動小数点型の場合、引数のサイズは 8 バイトに設定されます。 効果は、引数が 64 ビット レジスタの最下位ビットにコピーされ、残りのビットが未指定の値で埋められた場合と同じになります。
引数が HFA、HVA、半精度、単精度、倍精度、四倍精度浮動小数点型、またはショート ベクター型である場合、引数は調整された NSAA のメモリにコピーされます。 NSAA が引数のサイズだけインクリメントされます。 これで引数が割り当てられました。
引数が整数型またはポインター型で、引数のサイズが 8 バイト以下、NGRN が 8 バイト未満の場合、引数は、x[NGRN] の最下位ビットにコピーされます。 NGRN は 1 ずつインクリメントされます。 これで引数が割り当てられました。
引数のアラインメントが 16 の場合、NGRN は次の偶数に切り上げられます。
引数が整数型で、引数のサイズが 16、NGRN が 7 未満の場合、引数は x[NGRN] と x[NGRN + 1] にコピーされます。 x[NGRN] には、引数のメモリ表現の低位アドレスのダブルワードが含まれている必要があります。 NGRN は 2 ずつインクリメントされます。 これで引数が割り当てられました。
引数が複合型で、引数のダブルワードのサイズが 8 から NGRN を引いた値以下である場合、引数は x[NGRN] から始まる連続する汎用レジスタにコピーされます。 この引数は、メモリから連続してレジスタを読み込む LDR 命令の適切なシーケンスを使用して、ダブルワードでアラインされたアドレスからレジスタに読み込まれているかのように渡されます。 レジスタの未使用部分の内容は、この標準では指定されていません。 NGRN は使用したレジスタの数だけインクリメントされます。 これで引数が割り当てられました。
NGRN は 8 に設定されます。
NSAA は、8 またはその引数の型の自然なアラインメントのより大きい方に切り上げられます。
引数が複合型の場合、引数は調整された NSAA のメモリにコピーされます。 NSAA が引数のサイズだけインクリメントされます。 これで引数が割り当てられました。
引数のサイズが 8 バイト未満の場合、引数のサイズは 8 バイトに設定されます。 効果は、引数が 64 ビット レジスタの最下位ビットにコピーされ、残りのビットが未指定の値で埋められた場合と同じになります。
引数は調整された NSAA のメモリにコピーされます。 NSAA が引数のサイズだけインクリメントされます。 これで引数が割り当てられました。
補遺: 可変関数
可変個の引数を受け取る関数は、次のように、上記とは異なる方法で処理されます。
すべての複合は同様に扱われます。HFA や HVA に特別な処理はありません。
SIMD および浮動小数点レジスタは使用されません。
実質的には、規則 C.12–C.15 に従って引数を虚数スタックに割り当てるのと同じです。この場合、スタックの最初の 64 バイトは x0-x7 に読み込まれ、残りのスタックの引数は正常に配置されます。
戻り値
整数値は x0 に返されます。
浮動小数点の値は、s0、d0、または v0 に適宜返されます。
次のすべてが成り立つ場合、型は HFA または HFA と見なされます。
- 空ではありません。
- 非単純な既定値またはコピー コンストラクター、デストラクター、または代入演算子がありません。
- そのすべてのメンバーが、同じ HFA または HFA 型であるか、または他のメンバーの HFA または HFA 型と一致する float、double、または neon 型です。
4 つ以下の要素を持つ HVA 値は、必要に応じて、s0-s3、d0-d3、または v0-v3 で返されます。
値で返される型は、特定のプロパティがあるかどうか、および関数が静的でないメンバー関数であるかどうかによって、異なる方法で処理されます。 型に次のプロパティがすべて含まれる場合:
- C++14 標準定義で集約される。つまり、ユーザー指定のコンストラクターがなく、プライベートまたは保護された非静的データ メンバーを持たず、基底クラスや仮想関数もありません。
- トリビアルなコピー代入演算子がある。
- トリビアルなデストラクターがある。
この場合は、次の戻り値のスタイルを使用して、非メンバー関数または静的メンバー関数によって返されます。
- 要素が 4 つ以下の HFA の型は、必要に応じて s0-s3、d0-d3、または v0-v3 で返されます。
- 8 バイト以下の型は x0 で返されます。
- 16 バイト以下の型は x0 と x1 で返され、x0 には下位 8 バイトが含まれます。
- 他の集計型の場合、呼び出し元は、結果を保持するのに十分なサイズとアラインメントのメモリ ブロックを予約する必要があります。 メモリ ブロックのアドレスは、x8 内の関数に追加の引数として渡す必要があります。 呼び出し先は、サブルーチンの実行中の任意の時点で、結果のメモリ ブロックを変更する場合があります。 呼び出し先は、x8 に格納されている値を保持する必要はありません。
他のすべての型は、次の規則を使用します。
- 呼び出し元は、十分なサイズのメモリ ブロックと、結果を保持するためのアラインメントを予約する必要があります。 メモリ ブロックのアドレスは、x0 内の関数に追加の引数として渡す必要があります。また、$this が x0 に渡される場合は、x1 に渡す必要があります。 呼び出し先は、サブルーチンの実行中の任意の時点で、結果のメモリ ブロックを変更する場合があります。 呼び出し先は、x0 内のメモリ ブロックのアドレスを返します。
Stack
ARM によって提示された ABI に従い、スタックは常に 16 バイトでアラインされた状態である必要があります。 AArch64 には、SP が 16 バイトでアラインされていない状態で SP 関連の読み込みまたは格納が行われると、スタック アラインメント エラーが生成されるいうハードウェア機能が含まれています。 Windows は、この機能が常に有効になっている状態で実行されます。
スタックに 4K 以上相当のスタックを割り当てる関数は、最終ページの前のページが順番に接していることを確認する必要があります。 このアクションにより、Windows がスタックを拡張するために使用するガード ページをコードが "飛び越え" ないようにできます。 通常、タッチは __chkstk
ヘルパーによって行われます。これには、合計スタック割り当てを 16 で除算したものを x15 に渡す、カスタム呼び出し規則があります。
レッド ゾーン
現在のスタック ポインターの直下にある 16 バイト領域は、解析および動的なパッチのシナリオのために予約されています。 この領域には、細心の注意を払って生成されたコード ([sp, #-16] に 2 つのレジスタを格納し、一時的に任意の目的で使用できる) を挿入できます。 Windows カーネルにより、ユーザー モードおよびカーネル モードで例外または割り込みが発生した場合でも、この 16 バイト領域は上書きされないことが保証されます。
カーネル スタック
Windows の既定のカーネル モード スタックは 6 ページ (24k) です。 カーネル モードで大きなスタック バッファーを使用する関数には、特に注意してください。 悪いタイミングでの割り込みには小さなヘッドルームが伴い、スタック パニック バグ チェックが作成される場合があります。
スタック ウォーキング
Windows 内のコードは、ファスト スタック ウォーキングを可能にするために、フレーム ポインターを有効にした状態 (/Oy-) でコンパイルされています。 一般的に、x29 (fp) は、チェーンの次のリンクである {fp, lr} ペアを示します。このペアは、スタック上の前のフレームへのポインターを指定し、アドレスを返します。 サードパーティのコードでは、プロファイルとトレースを向上させるために、フレーム ポインターも有効にすることをお勧めします。
例外アンワインド
例外処理中のアンワインドは、アンワインド コードの使用を通じて支援されます。 アンワインド コードは、実行可能ファイルの .xdata セクションに格納されているバイト シーケンスです。 これらには、呼び出し元のスタック フレームへのバックアップの準備段階で関数のプロローグの効果を元に戻すことができるよう、プロローグおよびエピローグの操作が抽象的に示されています。 アンワインド コードの詳細については、「ARM64 例外処理」を参照してください。
また、ARM EABI では、アンワインド コードを使用する例外アンワインド モデルが指定されています。 ただし、Windows のアンワインドでは、PC が関数のプロローグまたはエピローグの中間に存在するケースを取り扱う必要があるため、提示されている仕様では不十分です。
動的に生成されるコードは、生成されたコードが例外処理に関与できるように、RtlAddFunctionTable
および関連する関数を介して動的な関数テーブルを使用して記述してください。
サイクル カウンター
ARMv8 のすべての CPU は、ユーザー モードを含む任意の例外レベルで読み取り可能になるように Windows で構成される 64 ビット レジスタである、サイクル カウンター レジスタをサポートする必要があります。 これには、アセンブリ コードで MSR オペコードを使用するか、C/C++ コードの _ReadStatusReg
組み込みを使用して、特殊な PMCCNTR_EL0 レジスタを介してアクセスできます。
ここに示すサイクル カウンターは、実時間ではなく、実際のサイクル カウンターです。 カウント周波数は、プロセッサの周波数によって異なります。 サイクル カウンターの周波数を把握しておく必要がある場合は、サイクル カウンターを使用しないでください。 代わりに、実時間を測定するために、QueryPerformanceCounter
を使用してください。