ARM32 ABI 規則の概要
ARM プロセッサ上の Windows 用にコンパイルされたコードのアプリケーション バイナリ インターフェイス (ABI) は、標準の ARM EABI に基づいています。 この記事では、ARM 上の Windows と標準との主な相違点を取り上げています。 このドキュメントでは、ARM32 ABI について説明します。 ARM64 ABI の詳細については、「ARM64 ABI 規則の概要」を参照してください。 標準 ARM EABI の詳細については、「ARM アーキテクチャ用のアプリケーション バイナリ インターフェイス (ABI)」(外部リンク) を参照してください。
基本要件
ARM 上の Windows は、常に ARMv7 アーキテクチャで実行されていることを前提としています。 ハードウェアで VFPv3-D32 以降の形式の浮動小数点がサポートされている必要があります。 VFP が、ハードウェアで単精度と倍精度の両方の浮動小数点をサポートしている必要があります。 Windows ランタイムでは、非 VFP ハードウェア上での実行を可能にするための浮動小数点のエミュレーションがサポートされていません。
Advanced SIMD 拡張 (NEON) もハードウェアでサポートされている必要があります (整数と浮動小数点の両方の演算が含まれます)。 エミュレーション用のランタイム サポートは用意されていません。
整数の除算のサポート (UDIV/SDIV) をお勧めしますが必須ではありません。 整数の除算をサポートしないプラットフォームでは、これらの演算をトラップし、場合によっては修正する必要があるため、パフォーマンスが低下する可能性があります。
エンディアン
ARM 上の Windows は、リトル エンディアン モードで実行されます。 MSVC コンパイラと Windows ランタイムは、両方とも、常にリトルエンディアン データを想定しています。 ARM 命令セット アーキテクチャ (ISA) の SETEND 命令では、ユーザー モードのコードでも現在のエンディアンを変更できます。 しかし、アプリケーションにとって危険なため、これを行うことは推奨されません。 例外がビッグエンディアン モードで生成される場合、動作は予測できません。 ユーザー モードでアプリケーションが失敗するか、カーネル モードでバグチェックが行われる可能性があります。
配置
Windows により、ARM ハードウェアは、適切にアラインされていない整数アクセスを透過的に処理できるようになりますが、状況によってはアラインメント エラーが生成される場合があります。 アラインメントについては、次の規則に従ってください。
ハーフ ワード サイズ (16 ビット) およびワード サイズ (32 ビット) の整数の読み込みおよび格納はアラインする必要がありません。 ハードウェアにより効率的かつ透過的に処理されます。
浮動小数点の読み込みおよび格納はアラインする必要があります。 カーネルは、アラインされていない読み込みおよび格納を透過的に処理しますが、オーバーヘッドが著しく大きくなります。
読み込みおよび格納の倍精度浮動小数点 (LDRD/STRD) および複合 (LDM/STM) 演算はアラインする必要があります。 カーネルは、大部分を透過的に処理しますが、オーバーヘッドが著しく大きくなります。
キャッシュされていないメモリ アクセスは、整数アクセスの場合でもすべてアラインする必要があります。 アラインされていないアクセスにより、アラインメント エラーが発生します。
命令セット
ARM 上の Windows の命令セットは Thumb-2 に厳密に制限されています。 このプラットフォームで実行されるすべてのコードは、常に Thumb モードで開始され、その状態が維持されることが想定されています。 レガシ ARM 命令セットに切り替える試みは成功する可能性があります。 ただし、その場合、例外や割り込みが発生すると、ユーザー モードでアプリケーションが失敗したり、カーネル モードでバグチェックが行われたりする可能性があります。
この要件の副作用として、すべてのコード ポインターは、下位ビット セットを持つ必要があります。 その後、BLX または BX を介して読み込まれ、分岐されても、プロセッサは Thumb モードのままです。 ターゲット コードを 32 ビットの ARM 命令として実行しようとはしません。
SDIV/UDIV 命令
整数の除算命令である SDIV と UDIV の使用が完全にサポートされています (それらを処理するネイティブ ハードウェアがないプラットフォームであっても)。 Cortex-A9 プロセッサでの SDIV または UDIV の除算ごとの追加のオーバーヘッドは、およそ 80 サイクルです。 これは、入力に応じて、20 から 250 サイクルの除算時間全体に追加されます。
整数レジスタ
ARM プロセッサでは、次の 16 種類の整数レジスタをサポートします。
登録 | Volatile? | Role |
---|---|---|
r0 | Volatile | パラメーター、結果、スクラッチ レジスタ 1 |
r1 | Volatile | パラメーター、結果、スクラッチ レジスタ 2 |
r2 | Volatile | パラメーター、スクラッチ レジスタ 3 |
r3 | Volatile | パラメーター、スクラッチ レジスタ 4 |
r4 | 非 volatile | |
r5 | 非 volatile | |
r6 | 非 volatile | |
r7 | 非 volatile | |
r8 | 非 volatile | |
r9 | 非 volatile | |
r10 | 非 volatile | |
r11 | 非 volatile | フレーム ポインター |
r12 | Volatile | プロシージャ内呼び出しスクラッチ レジスタ |
r13 (SP) | 非 volatile | スタック ポインター |
r14 (LR) | 非 volatile | リンク レジスタ |
r15 (PC) | 非 volatile | プログラム カウンター |
パラメーターと戻り値のレジスタの使用方法については、この記事の「パラメーターの引き渡し」のセクションを参照してください。
Windows では、スタック フレームのファスト ウォーキングに r11 を使用します。 詳細については、「スタック ウォーキング」のセクションを参照してください。 この要件のため、r11 は常にチェーンの最上位のリンクを指す必要があります。 r11 は汎用では使用しないでください。使用すると、解析中にコードが正しいスタック ウォークを生成しなくなります。
VFP レジスタ
Windows では、VFPv3-D32 コプロセッサをサポートする ARM バリアントのみがサポートされます。 これは、浮動小数点レジスタが常に存在し、パラメーター渡しに依存できることを意味します。 また、32 レジスタの完全なセットを使用できます。 次の表に、VFP レジスタとその使用方法の概要を示します。
単精度 | 倍精度 | 四倍精度 | Volatile? | Role |
---|---|---|---|---|
s0-s3 | d0-d1 | q0 | Volatile | パラメーター、結果、スクラッチ レジスタ |
s4-s7 | d2-d3 | q1 | Volatile | パラメーター、スクラッチ レジスタ |
s8-s11 | d4-d5 | q2 | Volatile | パラメーター、スクラッチ レジスタ |
s12-s15 | d6-d7 | q3 | Volatile | パラメーター、スクラッチ レジスタ |
s16-s19 | d8-d9 | q4 | 非 volatile | |
s20-s23 | d10-d11 | q5 | 非 volatile | |
s24-s27 | d12-d13 | q6 | 非 volatile | |
s28-s31 | d14-d15 | q7 | 非 volatile | |
d16-d31 | q8-q15 | Volatile |
次の表に、浮動小数点状態制御レジスタ (FPSCR) のビットフィールドを示します。
Bits | 意味 | Volatile? | Role |
---|---|---|---|
31-28 | NZCV | Volatile | ステータス フラグ |
27 | QC | Volatile | 累積的飽和 |
26 | AHP | 非 volatile | 代替の半制度制御 |
25 | DN | 非 volatile | 既定の NaN モード制御 |
24 | FZ | 非 volatile | Flush-to-zero モード制御 |
23-22 | RMode | 非 volatile | 丸めモード制御 |
21-20 | Stride | 非 volatile | ベクター ストライド。常に 0 であること |
18-16 | Len | 非 volatile | ベクター長。常に 0 であること |
15, 12-8 | IDE、IXE など | 非 volatile | 例外トラップ イネーブル ビット。常に 0 であること |
7, 4-0 | IDC、IXC など | Volatile | 累積的な例外フラグ |
浮動小数点例外
ほとんどの ARM ハードウェアは、IEEE 浮動小数点例外をサポートしません。 ハードウェア浮動小数点例外をサポートするプロセッサ バリアントでは、Windows カーネルが例外をサイレントにキャッチし、FPSCR レジスタで暗黙的に無効にします。 このアクションにより、プロセッサ バリアント全体で動作が正常に保たれます。 それ以外の場合は、例外をサポートしないプラットフォームで開発されたコードが、例外をサポートするプラットフォームで実行中の場合に、予期しない例外を受け取ることができます。
パラメーター渡し
ARM ABI 上の Windows は、非可変個引数関数のパラメーター渡しに関する ARM 規則に従います。 ABI 規則には、VFP と Advanced SIMD の拡張機能が含まれます。 これらの規則は、VFP 拡張機能と組み合わされた ARM アーキテクチャのプロシージャ呼び出し標準に従います。 既定では、最初の 4 個の整数引数と最大 8 個の浮動小数点またはベクター引数をレジスタに渡すことができます。 追加の引数はスタックで渡されます。 引数は、次のプロシージャを使用してレジスタまたはスタックに割り当てられます。
ステージ A: 初期化
初期化は、引数の処理が始まる前に 1 回のみ行われます。
次のコアのレジスタ番号 (NCRN) を r0 に設定します。
VFP レジスタが未割り当てとしてマークされます。
次のスタック引数アドレス (NSAA) が現在の SP に設定されます。
メモリに結果を返す関数が呼び出されると、結果のアドレスが r0 に配置され、NCRN が r1 に設定されます。
ステージ B: 引数の事前埋め込みと拡張
リストの各引数には、次のリストで最初に一致する規則が適用されます。
呼び出し元や呼び出し先からサイズを静的に決定できない複合型の引数の場合、引数はメモリにコピーされ、そのコピーへのポインターによって置き換えられます。
引数が 1 バイトまたは 16 ビット ハーフ ワードの場合、32 ビット フル ワードにゼロ拡張または符号拡張され、4 バイト引数として取り扱われます。
引数が複合型の場合、サイズが最も近い 4 の倍数に丸められます。
ステージ C: レジスタとスタックへの引数の割り当て
リストの各引数には、引数が割り当てられるまで次の規則が順番に適用されます。
引数が VFP 型で、適切な型の連続した未割り当ての VFP レジスタが十分にある場合、これらのレジスタのうち最小の番号のシーケンスに引数が割り当てられます。
引数が VFP 型の場合、残りのすべての未割り当てのレジスタは利用不可としてマークされます。 NSAA は、引数型と正しくアラインされるまで上方向に調節され、調節された NSAA で引数がスタックにコピーされます。 次に、NSAA が引数のサイズだけインクリメントされます。
引数が 8 バイト アラインメントを必要とする場合は、次の偶数のレジスタ番号まで NCRN が丸められます。
32 ビット ワードの引数のサイズが (r4 - NCRN) を超えない場合、最下位ビットが小さい番号のレジスタを占有した状態で、NCRN から始まるコア レジスタ内にその引数がコピーされます。 NCRN が使用したレジスタの数だけインクリメントされます。
NCRN が r4 未満で NSAA が SP と等しい場合、引数はコア レジスタとスタックに分割されます。 引数の最初の部分は、NCRN から始まり r3 までコア レジスタにコピーされます。 引数の残りは NSAA から始まるスタック上にコピーされます。 NCRN が r4 に設定され、(引数のサイズ - レジスタに引き渡された量) だけ NSAA がインクリメントされます。
引数が 8 バイト アラインメントを必要とする場合は、次の 8 バイトのアラインされたアドレスまで NSAA が丸められます。
引数は NSAA でメモリにコピーされます。 NSAA が引数のサイズだけインクリメントされます。
VFP レジスタは、可変個引数関数には使用されません。また、ステージ C の規則 1 および 2 は無視されます。 つまり、可変個引数関数をオプションの push {r0-r3} から開始して、呼び出し元より渡された任意の追加の引数の先頭にレジスタ引数を追加し、スタックから引数リスト全体に直接アクセスできます。
整数型の値は r0 に返され、オプションで 64 ビット戻り値の r1 に拡張されます。 VFP/NEON 浮動小数点または SIMD 型の値は、s0、d0 または q0 に適宜返されます。
Stack
スタックは、常に 4 バイトでアラインされ、関数境界では 8 バイトでアラインされている必要があります。 これは、64 ビット スタック変数で頻繁に使用されるインタロック操作をサポートするために必要です。 ARM EABI では、どのパブリック インターフェイスでもスタックが 8 バイトでアラインされていると示されています。 一貫性を確保するために、ARM ABI 上の Windows では、すべての関数境界がパブリック インターフェイスと見なされます。
フレーム ポインターを使用する必要がある関数 (たとえば、alloca
を呼び出す関数またはスタック ポインターを動的に変更する関数) は、フレーム ポインターを関数プロローグの r11 に設定し、エピローグまで変更しないでおく必要があります。 フレーム ポインターを必要としない関数は、プロローグですべてのスタック更新を実行し、エピローグまでスタック ポインターを変更しないでおきます。
スタックに 4 KB 以上を割り当てる関数は、最終ページの前のページが順番に接していることを確認する必要があります。 この順番により、Windows がスタックを拡張するために使用するガード ページをコードが "飛び越え" ないようにできます。 拡張は、一般的に、__chkstk
ヘルパーによって行われます。4 で除算されたスタック割り当ての合計 (バイト単位) が r4 で渡され、最終のスタック割り当て量 (バイト単位) が r4 で返されます。
レッド ゾーン
現在のスタック ポインターの直下にある 8 バイト領域は、解析および動的なパッチのために予約されています。 ここには、細心の注意を払って生成されたコード ([sp, #-8]
に 2 つのレジスタを格納し、一時的に任意の目的で使用できる) を挿入できます。 Windows カーネルでは、ユーザー モードとカーネル モードの両方で例外または割り込みが発生した場合でも、この 8 バイト領域は上書きされません。
カーネル スタック
Windows の既定のカーネル モード スタックは 3 ページ (12 KB) です。 カーネル モードでは、スタック バッファーの大きな関数を作成しないように注意してください。 非常に小さなスタック ヘッドルームで割り込みが行われ、スタック パニック バグチェックが生じる場合があります。
C/C++ 固有の仕様
列挙の値の少なくとも 1 つに 64 ビット ダブルワード ストレージが必要になる場合を除いて、列挙は 32 ビット整数型です。 そのような場合は、列挙が 64 ビット整数型に昇格されます。
他のプラットフォームとの互換性を維持するために、wchar_t
は unsigned short
と同等になるように定義されます。
スタック ウォーキング
Windows コードは、ファスト スタック ウォーキングを可能にするために、フレーム ポインターを有効にして (/Oy (フレーム ポインターの省略)) コンパイルされます。 一般的に、r11 レジスタは、チェーンの次のリンクである {r11, lr} ペアを示します。このペアは、スタック上の前のフレームへのポインターを指定し、アドレスを返します。 プロファイリングおよびトレースを向上させるために、コードでフレーム ポインターを有効にすることをお勧めします。
例外アンワインド
例外処理中のスタック アンワインドは、アンワインド コードを使用することで可能になります。 アンワインド コードは、実行可能イメージの .xdata セクションに格納されているバイト シーケンスです。 それらは、関数プロローグとエピローグ コードの操作を抽象的に記述し、呼び出し元のスタック フレームへのアンワインドの準備段階で関数のプロローグの効果を元に戻すことができるようにしています。
ARM EABI では、アンワインド コードを使用する例外アンワインド モデルが指定されています。 ただし、Windows のアンワインドでは、プロセッサが関数のプロローグまたはエピローグの中間に存在するケースを取り扱う必要があるため、この仕様は不十分です。 ARM 上の Windows の例外データおよびアンワインドの詳細については、「ARM の例外処理」を参照してください。
生成されたコードが例外処理に関与できるように、RtlAddFunctionTable
の呼び出しで指定された動的な関数テーブルおよび関連する関数を使用して、動的に生成されたコードを記述することをお勧めします。
サイクル カウンター
Windows を実行中の ARM プロセッサは、サイクル カウンターをサポートする必要がありますが、このカウンターを直接使用すると問題が発生する場合があります。 このような問題を回避するために、ARM 上の Windows は未定義のオペコードを使用し、正規化された 64 ビット サイクル カウンター値を要求します。 C または C++ からは __rdpmccntr64
組み込みを使用して適切なオペコードを出力し、アセンブリからは __rdpmccntr64
命令を使用します。 サイクル カウンターを読み取るには、Cortex-A9 で約 60 サイクルかかります。
カウンターはクロックではなく真のサイクル カウンターであるため、カウント周波数はプロセッサ周波数に従って変化します。 経過したクロック時間を測定する場合は、QueryPerformanceCounter
を使用します。