ARM64EC ABI 規則の概要

ARM64EC は、ARM64 バイナリを x64 コードとネイティブかつ相互運用可能に実行できるようにするアプリケーション バイナリ インターフェイス (ABI) です。 具体的には、ARM64EC ABI は呼び出し規則、スタックの使用法、データ アラインメントを含む x64 ソフトウェア規約に従い、ARM64EC および x64 コードを相互運用可能にします。 オペレーティング システムは、バイナリの x64 部分をエミュレートします。 (ARM64EC の中の EC は、"エミュレーション互換" を意味します。)

x64 および ARM64 の ABI の詳細については、「x64 ABI 規約の概要」と「ARM64 ABI 規約の概要」を参照してください。

ARM64EC は、x64 および ARM ベースのアーキテクチャ間のメモリ モデルの違いを解決しません。 詳細については、「一般的な Visual C++ ARM 移行に関する問題」を参照してください。

定義

  • ARM64 - 従来の ARM64 コードを含む ARM64 プロセス用のコード ストリーム。
  • ARM64EC - ARM64 レジスタ セットのサブセットを利用して x64 コードとの相互運用性を提供するコード ストリーム。

レジスタ マッピング

x64 プロセスは、ARM64EC コードを実行しているスレッドを持つことができます。 そのため、x64 レジスタ コンテキストを取得することは常に可能で、ARM64EC は、エミュレートされた x64 レジスタに 1:1 でマップされる ARM64 コア レジスタのサブセットを使用します。 重要なこととして、ARM64EC は、スレッド環境ブロック (TEB) アドレスを x18 から読み取る場合を除き、このサブセット以外のレジスタを使用することはありません。

ネイティブ ARM64 プロセスは、一部または多くの関数が ARM64EC として再コンパイルされてもパフォーマンスを低下させるべきではありません。 パフォーマンスを維持するために、この ABI は以下の原則に従います。

  • ARM64EC レジスタ サブセットには、ARM64 関数呼び出し規則に含まれるすべてのレジスタが含まれる。

  • ARM64EC 呼び出し規則は、ARM64 呼び出し規則に直接マップされる。

__chkstk_arm64ec のような特殊なヘルパー ルーチンは、カスタムの呼び出し規則とレジスタを使用します。 これらのレジスタも、ARM64EC のレジスタ サブセットに含まれます。

整数レジスタ用のレジスタ マッピング

ARM64EC レジスタ x64 レジスタ ARM64EC 呼び出し規則 ARM64 呼び出し規則 x64 での呼び出し規則
x0 rcx volatile volatile volatile
x1 rdx volatile volatile volatile
x2 r8 volatile volatile volatile
x3 r9 volatile volatile volatile
x4 r10 volatile volatile volatile
x5 r11 volatile volatile volatile
x6 mm1 (x87 R1 レジスタの下位 64 ビット) volatile volatile volatile
x7 mm2 (x87 R2 レジスタの下位 64 ビット) volatile volatile volatile
x8 rax volatile volatile volatile
x9 mm3 (x87 R3 レジスタの下位 64 ビット) volatile volatile volatile
x10 mm4 (x87 R4 レジスタの下位 64 ビット) volatile volatile volatile
x11 mm5 (x87 R5 レジスタの下位 64 ビット) volatile volatile volatile
x12 mm6 (x87 R6 レジスタの下位 64 ビット) volatile volatile volatile
x13 該当なし disallowed volatile 該当なし
x14 該当なし disallowed volatile 該当なし
x15 mm7 (x87 R7 レジスタの下位 64 ビット) volatile volatile volatile
x16 x87 R0-R3 レジスタのそれぞれの上位 16 ビット volatile(xip0) volatile(xip0) volatile
x17 x87 R4-R7 レジスタのそれぞれの上位 16 ビット volatile(xip1) volatile(xip1) volatile
x18 GS.base fixed(TEB) fixed(TEB) fixed(TEB)
x19 r12 non-volatile non-volatile non-volatile
x20 r13 non-volatile non-volatile non-volatile
x21 r14 non-volatile non-volatile non-volatile
x22 r15 non-volatile non-volatile non-volatile
x23 該当なし disallowed non-volatile 該当なし
x24 該当なし disallowed non-volatile 該当なし
x25 rsi non-volatile non-volatile non-volatile
x26 rdi non-volatile non-volatile non-volatile
x27 rbx non-volatile non-volatile non-volatile
x28 該当なし disallowed disallowed 該当なし
fp rbp non-volatile non-volatile non-volatile
lr mm0 (x87 R0 レジスタの下位 64 ビット) 両方 両方 両方
sp rsp non-volatile non-volatile non-volatile
pc rip 命令ポインター 命令ポインター 命令ポインター
PSTATE サブセット: N/Z/C/V/SS 1, 2 RFLAGS サブセット: SF/ZF/CF/OF/TF volatile volatile volatile
該当なし RFLAGS サブセット: PF/AF 該当なし 該当なし volatile
該当なし RFLAGS サブセット: DF 該当なし 該当なし non-volatile

1 PSTATERFLAGS の直接的な読み取り、書き込み、あるいはこれらの間のマッピングの計算は避けてください。 これらのビットは将来的に使用される可能性があり、変更される可能性があります。

2 ARM64EC のキャリー フラグ C は、減算演算の x64 キャリー フラグ CF を反転したものです。 このフラグは volatile であり、(ARM64EC と x64 の) 関数間の切り替え時に破棄されるため、特別な処理は必要ありません。

ベクトル レジスタのレジスタ マッピング

ARM64EC レジスタ x64 レジスタ ARM64EC 呼び出し規則 ARM64 呼び出し規則 x64 での呼び出し規則
v0-v5 xmm0-xmm5 volatile volatile volatile
v6-v7 xmm6-xmm7 volatile volatile non-volatile
v8-v15 xmm8-xmm15 volatile 1 volatile 1 non-volatile
v16-v31 xmm16-xmm31 disallowed volatile disallowed (x64 エミュレータは AVX-512 をサポートしていません)
FPCR 2 MXCSR[15:6] non-volatile non-volatile non-volatile
FPSR 2 MXCSR[5:0] volatile volatile volatile

1 これらの ARM64 レジスタは、下位 64 ビットが non-volatile である一方、上位 64 ビットは volatile であるという点で特別です。 呼び出し先がデータを破棄するため、x64 呼び出し元から見ると、これらは実質的に volatile です。

2 FPCRFPSR の直接的な読み取り、書き込み、あるいはマッピングの計算は避けてください。 これらのビットは将来的に使用される可能性があり、変更される可能性があります。

構造体パッキング

ARM64EC は、ARM64EC コードと x64 コード間の相互運用性を確保するために x64 に使用されるのと同じ構造体パッキング規則に従います。 x64 の構造体パッキングの詳細と例については、「x64 ABI 規約の概要」を参照してください。

エミュレーション ヘルパー ABI ルーチン

ARM64EC コードとサンクはエミュレーション ヘルパー ルーチンを使用して x64 および ARM64EC 関数間の切り替えを行います。

次の表は、それぞれの特殊な ABI ルーチンと、ABI が使用するレジスタについて説明したものです。 これらのルーチンは、ABI の列に記載されている保持レジスタを変更しません。 記載されていないレジスタに関してはどのような想定も行うべきではありません。 ディスク上では、ABI ルーチン ポインターは null です。 読み込み時に、ローダーは x64 エミュレータ ルーチンを指すようにポインターを更新します。

名前 説明 ABI
__os_arm64x_dispatch_call_no_redirect x64 ターゲット (x64 関数または x64 ファストフォワード シーケンス) を呼び出すために exit サンクによって呼び出されます。 このルーチンは、(LR レジスタ内の) ARM64EC リターン アドレスをプッシュし、その後には x64 エミュレータを呼び出す blr x16 命令に続く命令のアドレスが続きます。 次に、blr x16 命令を実行します x8 (rax) 内の戻り値
__os_arm64x_dispatch_ret x64 呼び出し元に戻るために entry サンクによって呼び出されます。 スタックから x64 リターン アドレスをポップし、x64 エミュレータを呼び出してそこへジャンプします 該当なし
__os_arm64x_check_call exit サンクへのポインターと、実行する間接 ARM64EC ターゲット アドレスを持つ ARM64EC コードによって呼び出されます。 ARM64EC ターゲットはパッチ適用可能と見なされ、実行は常に呼び出し時に使用されたデータまたは変更されたデータと共に呼び出し元に戻ります 引数:
x9: ターゲット アドレス
x10: exit サンク・アドレス
x11: ファスト フォワード シーケンス アドレス

出力:
x9: ターゲット関数が迂回された場合、これにはファスト フォワード シーケンスのアドレスが含まれています
x10: exit サンク・アドレス
x11: 関数が迂回された場合、これには exit サンク アドレスが含まれています。 それ以外の場合は、ジャンプ先のターゲット アドレスとなります

保持レジスタ: x0-x8x15 (chkstk)。 および q0-q7 に変更されました
__os_arm64x_check_icall x64 または ARM64EC のいずれかのターゲット アドレスへのジャンプを処理するために、exit サンクへのポインターと共に、ARM64EC コードによって呼び出されます。 ターゲットが x64 で、x64 コードにパッチが適用されていない場合、ルーチンはターゲット アドレス レジスタを設定します。 これは該当関数の ARM64EC バージョンを指します (存在する場合)。 それ以外の場合は、x64 ターゲットに遷移する exit サンクを指すようにレジスタを設定します。 それから、呼び出し元の ARM64EC コードに戻った後、レジスタ内のアドレスにジャンプします。 このルーチンは最適化されていないバージョンの __os_arm64x_check_call で、ターゲット アドレスはコンパイル時には不明です

間接呼び出しの呼び出しサイトで使用されます
引数:
x9: ターゲット アドレス
x10: exit サンク・アドレス
x11: ファスト フォワード シーケンス アドレス

出力:
x9: ターゲット関数が迂回された場合、これにはファスト フォワード シーケンスのアドレスが含まれています
x10: exit サンク・アドレス
x11: 関数が迂回された場合、これには exit サンク アドレスが含まれています。 それ以外の場合は、ジャンプ先のターゲット アドレスとなります

保持レジスタ: x0-x8x15 (chkstk)、q0-q7
__os_arm64x_check_icall_cfg __os_arm64x_check_icall と同じですが、指定されたアドレスが有効な制御フロー グラフの間接呼び出しターゲットであることも確認します 引数:
x10: exit サンクのアドレス
x11: ターゲット関数のアドレス

出力:
x9: ターゲットが x64 の場合は、関数のアドレス。 それ以外の場合は、未定義
x10: exit サンクのアドレス
x11: ターゲットが x64 の場合、これには exit サンクのアドレスが含まれています。 それ以外の場合は、関数のアドレス

保持レジスタ: x0-x8x15 (chkstk)、q0-q7
__os_arm64x_get_x64_information ライブ x64 レジスタ コンテキストの要求された部分を取得します _Function_class_(ARM64X_GET_X64_INFORMATION) NTSTATUS LdrpGetX64Information(_In_ ULONG Type, _Out_ PVOID Output, _In_ PVOID ExtraInfo)
__os_arm64x_set_x64_information ライブ x64 レジスタ コンテキストの要求された部分を設定します _Function_class_(ARM64X_SET_X64_INFORMATION) NTSTATUS LdrpSetX64Information(_In_ ULONG Type,_In_ PVOID Input, _In_ PVOID ExtraInfo)
__os_arm64x_x64_jump シグネチャのないアジャスタや、任意のシグネチャを持つ可能性のある別の関数へ呼び出しを直接転送 (jmp) するその他のサンク内で使用され、実際のターゲットに対する適切なサンクの適用を遅延させます 引数:
x9: ジャンプ先のターゲット

保持 (転送) されるすべてのパラメーター レジスタ

サンク

サンクは、ARM64EC 関数と x64 関数の相互呼び出しをサポートするための低レベルのメカニズムです。 ARM64EC 関数に入るための "entry サンク" と x64 関数を呼び出すための "exit サンク" という 2 種類が存在します。

entry サンクと intrinsic entry サンク: x64 から ARM64EC への関数呼び出し

C/C++ 関数が ARM64EC としてコンパイルされる場合の x64 呼び出し元をサポートするために、ツールチェーンは ARM64EC マシン コードから構成される単一の entry サンクを生成します。 intrinsic は独自の entry サンクを持ちます。 他のすべての関数は、一致する呼び出し規則、パラメーター、および戻り値の型を持つすべての関数と entry サンクを共有します。 サンクの内容は、C/C++ 関数の呼び出し規則によって決まります。

サンクは、パラメーターとリターン アドレスの処理に加えて、以下のように ARM64EC ベクトル レジスタ マッピングによって引き起こされる ARM64EC と x64 ベクトル レジスタ間のボラティリティの違いを埋めます。

ARM64EC レジスタ x64 レジスタ ARM64EC 呼び出し規則 ARM64 呼び出し規則 x64 での呼び出し規則
v6-v15 xmm6-xmm15 volatile ですが、entry サンク内で保存/復元されます (x64 から ARM64EC) volatile または部分的に volatile な上位 64 ビット non-volatile

entry サンクは、以下のアクションを実行します。

パラメーター番号 スタックの使用
0-4 ARM64EC v6v7 を呼び出し元割り当てされたホーム スペースに保存します

呼び出し先はホーム スペースの概念がない ARM64EC であるため、保存された値は上書きされません。

スタックに追加の 128 バイトを割り当て、v15 を通して ARM64EC v8 を保存します。
5-8 x4 = スタックの 5 番目のパラメーター
x5 = スタックの 6 番目のパラメーター
x6 = スタックの 7 番目のパラメーター
x7 = スタックの 8 番目のパラメーター

パラメーターが SIMD の場合は、代わりに v4-v7 レジスタが使用されます
+9 スタックに AlignUp(NumParams - 8 , 2) * 8 バイトを割り当てます。 *

9 番目と残りのパラメーターをこの領域にコピーします

* 値を偶数に揃えることで、スタックが 16 バイト アライメントされた状態が保たれることが保証されます

関数が 32 ビット整数パラメーターを受け取る場合、サンクがプッシュを許可されるのは親レジスタの 64 ビットすべてではなく、32 ビットだけになります。

次に、サンクは ARM64 bl 命令を使用して ARM64EC 関数を呼び出します。 関数がリターンした後、サンクは以下を行います。

  1. スタック割り当てを元に戻します
  2. __os_arm64x_dispatch_ret エミュレータ ヘルパーを呼び出して x64 リターン アドレスをポップし、x64 エミュレーションを再開します。

exit サンク: ARM64EC から x64 への関数呼び出し

ARM64EC C/C++ 関数が行う x64 であるかもしれないコードのすべての呼び出しに対して、MSVC ツールチェーンは exit サンクを生成します。 サンクの内容は、x64 呼び出し先のパラメーターと、呼び出し先が標準呼び出し規則と __vectorcall のどちらを使用しているかによって決まります。 コンパイラは、呼び出し先の関数宣言からこの情報を取得します。

まず、サンクはスタックが 16 バイト アライメントされることを保証するために、ARM64EC lr レジスタ内のリターン アドレスとダミーの 8 バイト値をプッシュします。 2 つ目に、サンクは以下のようにパラメーターを処理します。

パラメーター番号 スタックの使用
0-4 スタックに 32 バイトのホーム スペースを割り当てます
5-8 スタックの上方向にさらに AlignUp(NumParams - 4, 2) * 8 バイトを割り当てます。 *

5 番目以降のすべてのパラメーターを ARM64EC の x4-x7 からこの追加スペースにコピーします
+9 9 番目と残りのパラメーターを追加スペースにコピーします

* 値を偶数に揃えることで、スタックが 16 バイト アライメントされた状態が保たれることが保証されます。

3 つ目に、サンクは x64 エミュレータを呼び出して x64 関数を実行するために、__os_arm64x_dispatch_call_no_redirect エミュレータ ヘルパーを呼び出します。 この呼び出しは blr x16 命令である必要があります (都合のいいことに、x16 は volatile レジスタです)。 x64 エミュレータは blr x16 命令をヒントとして解析するため、この命令は必須です。

x64 関数は通常、x64 ret 命令を使用することでエミュレータ ヘルパーにリターンしようとします。 この時点で、x64 エミュレータはそれが ARM64EC コードであることを検出します。 次に、ちょうど ARM64 blr x16 命令である先ほどの 4 バイト ヒントを読み取ります。 このヒントは、リターン アドレスがこのヘルパー内にあることを示しているため、エミュレータはこのアドレスに直接ジャンプします。

x64 関数は、x64 jmpcallなど、任意の分岐命令を使用してエミュレータ ヘルパーに戻ることが許可されています。 エミュレータはこれらのシナリオも同様に処理します。

ヘルパーが次にサンクに戻ると、サンクは以下を行います。

  1. スタック割り当てをすべて元に戻します
  2. ARM64EC lr レジスタをポップします
  3. ARM64 ret lr 命令を実行します。

ARM64EC 関数名の装飾

ARM64EC 関数名は、言語固有の装飾の後に適用される 2 番目の装飾を持っています。 C リンケージを持つ関数 (C としてコンパイルされるか、extern "C" を使用するかに関係なく) に対しては、名前の先頭に # が付加されます。 C++ 装飾された関数に対しては、$$h タグが名前の中に挿入されます。

foo         => #foo
?foo@@YAHXZ => ?foo@@$$hYAHXZ

__vectorcall

現在、ARM64EC ツールチェーンは __vectorcall をサポートしていません。 コンパイラは、ARM64EC での __vectorcall の使用を検出するとエラーを発生させます。

関連項目

ARM64EC ABI とアセンブリ コードの理解
Visual C++ の ARM への移行に関する一般的な問題
装飾名