次の方法で共有


Arm64EC ABI とアセンブリ コードについて

Arm64EC ("エミュレーション互換") は、Arm 上の Windows 11 用アプリを構築するための新しいアプリケーション バイナリ インターフェイス (ABI) です。 Arm64EC の概要と、Arm64EC として Win32 アプリの構築を開始する方法については、「 Arm64EC を使用して Arm デバイスで Windows 11 用アプリを構築するを参照してください。

この記事では、Arm64EC ABI の詳細なビューと、アプリケーション開発者が Arm64EC 用にコンパイルされたコードを記述およびデバッグするための十分な情報を提供します。これには、低レベル/アセンブラーのデバッグや、Arm64EC ABI をターゲットとするアセンブリ コードの記述が含まれます。

Arm64EC の設計

Arm64EC はネイティブ レベルの機能とパフォーマンスを提供すると同時に、エミュレーションで実行される x64 コードとの透過的で直接的な相互運用性を提供します。

Arm64EC は、主に従来の Arm64 ABI に対する添加剤です。 従来の ABI はほとんど変更されませんが、Arm64EC ABI では x64 の相互運用性を実現する部分が追加されました。

このドキュメントでは、元の標準 Arm64 ABI を "クラシック ABI" と呼びます。 この用語は、"Native" のようなオーバーロードされた用語に固有のあいまいさを回避します。 Arm64EC は、元の ABI と同様にネイティブです。

Arm64EC と Arm64 クラシック ABI

次の一覧は、Arm64EC が Arm64 クラシック ABI から分岐する場所を示しています。

これらの違いは、ABI 全体が定義する量の観点から見ると、小さな変化です。

レジスタ マッピングとブロックされたレジスタ

x64 コードとの型レベルの相互運用性を有効にするために、Arm64EC コードは x64 コードと同じプリプロセッサ アーキテクチャ定義を使用してコンパイルします。

つまり、 _M_AMD64_AMD64_ が定義されます。 この規則の影響を受ける型の 1 つは、 CONTEXT 構造体です。 CONTEXT構造体は、特定の時点での CPU の状態を定義します。 これは、 Exception Handling API や GetThreadContext API などに使用されます。 既存の x64 コードでは、CPU コンテキストが x64 CONTEXT 構造体として表されるか、つまり、x64 コンパイル中に定義されている CONTEXT 構造体として表されます。

この構造体を使用して、x64 コードと Arm64EC コードの実行中に CPU コンテキストを表す必要があります。 既存のコードでは、CPU レジスタ セットが関数から関数に変更されるなど、新しい概念を理解していません。 x64 CONTEXT 構造体を使用して Arm64 の実行状態を表す場合は、Arm64 レジスタを x64 レジスタに効果的にマップします。

このマッピングは、x64 CONTEXTに収まらない Arm64 レジスタを使用できないことも意味します。 これらの値は、操作が CONTEXT を使用するたびに失われる可能性があります (また、マネージド言語ランタイムのガベージ コレクション操作や APC など、一部の操作は非同期で予期しない場合があります)。

SDK の Windows ヘッダーは、arm64EC と x64 レジスタの間のマッピング 規則を ARM64EC_NT_CONTEXT 構造で表します。 この構造体は基本的に、x64 用に定義されているのとまったく同じように、 CONTEXT 構造体の和集合ですが、追加の Arm64 レジスタ オーバーレイを使用します。

たとえば、RCXX0に、RDXX1に、RSPSPに、RIPPCに対応します、などです。 レジスタx13x14x23x24x28、およびv16v31には表現がないため、Arm64EC では使用できません。

このレジスタの使用制限は、Arm64 クラシック ABIs と EC ABI の最初の違いです。

通話チェッカー

呼び出しチェッカーは、Windows 8.1 で Control Flow Guard (CFG) が導入されて以来、これまで Windows の一部でした。 呼び出しチェッカーは、(これらのことがアドレスサニタイザーと呼ばれる前に) 関数ポインターのアドレスサニタイザーです。 オプション /guard:cfを使用してコードをコンパイルするたびに、コンパイラは、すべての間接呼び出しまたはジャンプの直前にチェッカー関数への追加の呼び出しを生成します。 Windows にはチェッカー関数自体が用意されています。 CFG の場合、既知のto-be-good 呼び出しターゲットに対して有効性チェックを実行します。 /guard:cfでコンパイルされたバイナリにも、この情報が含まれます。

この例は、クラシック Arm64 での通話チェッカーの使用を示しています。

mov     x15, <target>
adrp    x16, __guard_check_icall_fptr
ldr     x16, [x16, __guard_check_icall_fptr]
blr     x16                                     ; check target function
blr     x15                                     ; call function

CFG の場合、呼び出しチェッカーはターゲットが有効であれば単に戻り、無効であればプロセスを即座に中断します。 呼び出しチェッカーには、カスタム呼び出し規則があります。 通常の呼び出し規約で使用されていないレジスタ内の関数ポインターを受け取り、すべての通常の呼び出し規約レジスタを保持します。 この方法では、レジスタの流出は発生しません。

呼び出しチェッカーは、他のすべての Windows AVI では省略可能ですが、Arm64EC では必須です。 Arm64EC では、呼び出しチェッカーは、呼び出される関数のアーキテクチャを検証するタスクを蓄積します。 呼び出しが、エミュレーションで実行する必要がある別の EC ("エミュレーション互換") 関数または x64 関数であるかどうかを確認します。 多くの場合、これは実行時にのみ検証できます。

Arm64EC 呼び出しチェッカーは、既存の Arm64 チェッカーの上に構築されますが、カスタム呼び出し規則が若干異なります。 追加のパラメーターを受け取り、ターゲット アドレスを含むレジスタを変更できます。 たとえば、ターゲットが x64 コードの場合は、まずエミュレーション スキャフォールディング ロジックに制御を転送する必要があります。

Arm64EC では、同じ呼び出しチェッカーを使用すると次のようになります。

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, <name of the exit thunk>
add     x10, x10, <name of the exit thunk>
blr     x9                                      ; check target function
blr     x11                                     ; call function

従来の Arm64 とのわずかな違いは次のとおりです。

  • 呼び出しチェッカーのシンボル名が異なります。
  • ターゲット アドレスは、x11ではなく、x15で指定されます。
  • ターゲット アドレス (x11) は、[in, out]ではなく[in]されます。
  • "Exit Thunk" と呼ばれる、 x10を介して提供される追加のパラメーターがあります。

Exit Thunk は、関数パラメーターを Arm64EC 呼び出し規則から x64 呼び出し規則に変換する機能です。

Arm64EC 呼び出しチェッカーは、Windows の他の ABI に使用されるシンボルとは異なるシンボルを介して配置されます。 クラシック Arm64 ABI では、呼び出しチェッカーのシンボルは __guard_check_icall_fptr。 このシンボルは Arm64EC に存在しますが、Arm64EC コード自体ではなく、x64 静的にリンクされたコードが使用されます。 Arm64EC コードでは、 __os_arm64x_check_icall または __os_arm64x_check_icall_cfgが使用されます。

Arm64EC では、呼び出しチェッカーは省略可能ではありません。 ただし、他の ABI の場合と同様に、CFG は引き続き省略可能です。 CFG はコンパイル時に無効にされるか、CFG が有効になっている場合でも CFG チェックを実行しない正当な理由がある場合があります (たとえば、関数ポインターが RW メモリに存在しない場合など)。 CFG チェックを使用した間接呼び出しの場合は、 __os_arm64x_check_icall_cfg チェッカーを使用する必要があります。 CFG が無効になっているか不要な場合は、代わりに __os_arm64x_check_icall を使用する必要があります。

以下は、従来の Arm64、x64、Arm64EC での呼び出しチェッカーの使用方法の概要表です。Arm64EC バイナリには、コードのアーキテクチャに応じて 2 つのオプションがあります。

バイナリ Code 保護されていない間接呼び出し CFG で保護された間接呼び出し
X64 X64 通話チェッカーなし __guard_check_icall_fptr または __guard_dispatch_icall_fptr
Arm64 クラシック Arm64 通話チェッカーなし __guard_check_icall_fptr
Arm64EC X64 通話チェッカーなし __guard_check_icall_fptr または __guard_dispatch_icall_fptr
Arm64EC __os_arm64x_check_icall __os_arm64x_check_icall_cfg

ABI とは別に、CFG 対応コード (CFG 呼び出しチェッカーを参照するコード) を持つことは、実行時の CFG 保護を意味するものではありません。 CFG で保護されたバイナリは、CFG をサポートしていないシステムでダウンレベルで実行できます。呼び出しチェッカーはコンパイル時に no-op ヘルパーで初期化されます。 プロセスでは、構成によって CFG が無効になっている場合もあります。 以前の AVI で CFG が無効になっている (または OS のサポートが存在しない) 場合、OS はバイナリの読み込み時に呼び出しチェッカーを更新しません。 Arm64EC では、CFG 保護が無効になっている場合、OS は __os_arm64x_check_icall_cfg と同じ__os_arm64x_check_icall設定されます。これは、CFG 保護ではなく、すべてのケースで必要なターゲット アーキテクチャ チェックを引き続き提供します。

クラシック Arm64 の CFG と同様に、ターゲット関数 (x11) の呼び出しは、呼び出しチェッカーの呼び出しの直後に行う必要があります。 呼び出しチェッカーのアドレスは揮発性レジスタに配置する必要があり、別のレジスタにコピーしたり、メモリに書き込んだりすることはできません。

スタック チェッカー

__chkstk は、関数がページより大きいスタック上の領域を割り当てるたびに、コンパイラによって自動的に使用されます。 スタックの末尾を保護するスタック ガード ページをスキップしないようにするために、 __chkstk を呼び出して、割り当てられた領域内のすべてのページがプローブされるようにします。

__chkstk は通常、関数のプロローグから呼び出されます。 そのため、最適なコード生成のために、カスタム呼び出し規則が使用されます。

これは、x64 コードと Arm64EC コードには、Entry と Exit サンクが標準の呼び出し規則を前提としており、独自の個別の __chkstk 関数が必要であることを意味します。

x64 と Arm64EC は同じシンボル名前空間を共有するため、 __chkstkという名前の 2 つの関数を指定することはできません。 既存の x64 コードとの互換性に対応するために、 __chkstk 名は x64 スタック チェッカーに関連付けられます。 Arm64EC コードでは、代わりに __chkstk_arm64ec が使用されます。

__chkstk_arm64ecのカスタム呼び出し規則は、クラシック Arm64 __chkstkの場合と同じです。x15は、割り当てのサイズをバイト単位で 16 で割って提供します。 すべての非揮発性レジスタと、標準呼び出し規則に関係するすべての揮発性レジスタは保持されます。

上記の __chkstk に関するすべての内容は、 __security_check_cookie とそれに対応する Arm64EC ( __security_check_cookie_arm64ec) にも同様に適用されます。

可変個引数呼び出し規則

Arm64EC は、可変関数 (varargs または省略記号 (..) パラメーター キーワードを持つ関数とも呼ばれます) を除き、従来の Arm64 ABI 呼び出し規則に従います。

可変数固有のケースでは、Arm64EC は x64 可変数に非常によく似た呼び出し規則に従い、わずかな違いしかありません。 次の一覧は、Arm64EC 可変性の主要な規則を示しています。

  • パラメーターの受け渡しには、最初の 4 つのレジスタ ( x0x1x2x3) のみが使用されます。 残りのパラメーターはスタックに溢れます。 この規則は x64 可変呼び出し規則に厳密に従っており、x0を介してx7レジスタが使用される Arm64 クラシックとは異なります。
  • 浮動小数点パラメーターと SIMD パラメーターは、レジスタによって渡される際に、SIMD レジスタではなく汎用レジスタを使用します。 この規則は Arm64 クラシックに似ていますが、汎用レジスタと SIMD レジスタの両方で FP/SIMD パラメーターが渡される x64 とは異なります。 たとえば、f1(int, …)として呼び出f1(int, double)関数の場合、x64 では、2 番目のパラメーターがRDXXMM1の両方に割り当てられます。 Arm64EC では、2 番目のパラメーターは x1のみに割り当てられます。
  • レジスタを介して値によって構造体を渡す場合、x64 サイズルールが適用されます。サイズが正確に 1、2、4、8 バイトの構造体は汎用レジスタに直接読み込まれます。 異なるサイズの構造体がスタックにスピルされ、スピルされた場所へのポインタがレジスタに割り当てられます。 このルールは本質的に、低レベルで値渡しを参照渡しに変換します。 クラシック Arm64 ABI では、最大 16 バイトの任意のサイズの構造体が汎用レジスタに直接割り当てられます。
  • x4 レジスタは、スタック経由で渡された最初のパラメーター (5 番目のパラメーター) へのポインターを読み込みます。 この規則には、以前に示したサイズ制限のためにこぼれた構造体は含まれません。
  • x5 レジスタは、スタックで渡されるすべてのパラメーターのサイズ (バイト単位) を読み込みます (すべてのパラメーターのサイズ (5 番目以降)。 この規則には、前述のサイズ制限により値渡しされた際に漏れた構造体は含まれません。

次の例では、 pt_nova_function は非可変形式のパラメーターを受け取るので、従来の Arm64 呼び出し規則に従います。 その後、まったく同じパラメーターを使用して pt_va_function を呼び出しますが、代わりに可変呼び出しで呼び出します。

struct three_char {
    char a;
    char b;
    char c;
};

void
pt_va_function (
    double f,
    ...
);

void
pt_nova_function (
    double f,
    struct three_char tc,
    __int64 ull1,
    __int64 ull2,
    __int64 ull3
)
{
    pt_va_function(f, tc, ull1, ull2, ull3);
}

pt_nova_function は、クラシック Arm64 呼び出し規則規則に従って割り当てる 5 つのパラメーターを受け取ります。

  • 'f' は double です。 d0に割り当てられます。
  • 'tc' は、サイズが 3 バイトの構造体です。 x0に割り当てられます。
  • ull1 は 8 バイトの整数です。 x1に割り当てられます。
  • ull2 は 8 バイトの整数です。 x2に割り当てられます。
  • ull3 は 8 バイトの整数です。 x3に割り当てられます。

pt_va_function は可変関数であるため、前に説明した Arm64EC 可変規則に従います。

  • 'f' は double です。 x0に割り当てられます。
  • 'tc' は、サイズが 3 バイトの構造体です。 それはスタックに溢れ出し、その位置が x1 に読み込まれます。
  • ull1 は 8 バイトの整数です。 x2に割り当てられます。
  • ull2 は 8 バイトの整数です。 x3に割り当てられます。
  • ull3 は 8 バイトの整数です。 スタックに直接割り当てられます。
  • x4 は、スタック内の ull3 の場所を読み込みます。
  • x5 は、 ull3のサイズを読み込みます。

次の例は、前に説明したパラメーターの割り当ての違いを示す、 pt_nova_functionで考えられるコンパイル出力を示しています。

stp         fp,lr,[sp,#-0x30]!
mov         fp,sp
sub         sp,sp,#0x10

str         x3,[sp]          ; Spill 5th parameter
mov         x3,x2            ; 4th parameter to x3 (from x2)
mov         x2,x1            ; 3rd parameter to x2 (from x1)
str         w0,[sp,#0x20]    ; Spill 2nd parameter
add         x1,sp,#0x20      ; Address of 2nd parameter to x1
fmov        x0,d0            ; 1st parameter to x0 (from d0)
mov         x4,sp            ; Address of the 1st in-stack parameter to x4
mov         x5,#8            ; Size of the in-stack parameter area

bl          pt_va_function

add         sp,sp,#0x10
ldp         fp,lr,[sp],#0x30
ret

ABI の追加

x64 コードとの透過的な相互運用性を実現するには、従来の Arm64 ABI に多くの追加を行います。 これらの追加により、Arm64EC と x64 の呼び出し規則の違いが処理されます。

次の一覧には、これらの追加が含まれています。

エントリーとエグジットのサンク

入出サンクは、Arm64EC 呼び出し規則 (ほぼ従来の Arm64 と同じ) を x64 呼び出し規則に変換します。その逆も可能です。

一般的な誤解は、すべての関数シグネチャに適用される 1 つの規則に従って呼び出し規則を変換できることです。 実際には、呼び出し規則にはパラメーター割り当て規則があります。 これらのルールはパラメーターの種類によって異なり、ABI と ABI は異なります。 その結果、ABI 間の変換は各関数シグネチャに固有であり、各パラメーターの型によって異なります。

次のような関数があるとします。

int fJ(int a, int b, int c, int d);

パラメーターの割り当ては次のように行われます。

  • Arm64: a -> x0, b -> x1, c -> x2, d -> x3
  • x64: -> RCX、b -> RDX、c -> R8、d -> r9
  • Arm64 -> x64 変換: x0 -> RCX、x1 -> RDX、x2 -> R8、x3 -> R9

次に、別の関数を検討します。

int fK(int a, double b, int c, double d);

パラメーターの割り当ては次のように行われます。

  • Arm64: a -> x0, b -> d0, c -> x1, d -> d1
  • x64: -> RCX、b -> XMM1、c -> R8、d -> XMM3
  • Arm64 -> x64 変換: x0 -> RCX、d0 -> XMM1、x1 -> R8、d1 -> XMM3

これらの例では、パラメーターの割り当てと変換は型によって異なりますが、一覧の前のパラメーターの型にも依存することを示しています。 この詳細は、3 番目のパラメーターで示されています。 どちらの関数でも、パラメーターの型は intされますが、結果の変換は異なります。

この理由から、入退出サンクが存在し、個々の関数シグネチャに合わせて特別に調整されています。

どちらの種類のサンクも関数です。 x64 関数が Arm64EC 関数を呼び出すと、エミュレーターは自動的にエントリー サンクを呼び出します(実行が Arm64EC に移行します)。 Arm64EC 関数が x64 関数を呼び出すと、呼び出しチェッカーによって終了サンクが自動的に呼び出されます (実行は Arm64EC を 終了 します)。

Arm64EC コードをコンパイルすると、コンパイラは各 Arm64EC 関数のエントリ サンクを生成し、そのシグネチャと一致します。 コンパイラは、Arm64EC 関数呼び出しのすべての関数に対して終了サンクも生成します。

次の例を確認してください。

struct SC {
    char a;
    char b;
    char c;
};

int fB(int a, double b, int i1, int i2, int i3);

int fC(int a, struct SC c, int i1, int i2, int i3);

int fA(int a, double b, struct SC c, int i1, int i2, int i3) {
    return fB(a, b, i1, i2, i3) + fC(a, c, i1, i2, i3);
}

Arm64EC を対象とする上記のコードをコンパイルすると、コンパイラによって次が生成されます。

  • fAのコード。
  • エントリーサンク fA
  • 終了時のサンク fB
  • fC の終了用サンクション

x64 コードからfAが呼び出された場合、コンパイラはfAエントリ サンクを生成します。 fBfCが x64 コードの場合、コンパイラはfBfCの終了サンクを生成します。

コンパイラは、関数自体ではなく呼び出しサイトで生成するため、同じ終了サンクを複数回生成する可能性があります。 この重複により、大量の冗長サンクが発生する可能性があります。 この重複を回避するために、コンパイラは単純な最適化規則を適用して、必要なサンクのみが最終的なバイナリになるようにします。

たとえば、Arm64EC 関数 A が Arm64EC 関数 Bを呼び出すバイナリでは、 B はエクスポートされず、そのアドレスは Aの外部では認識されません。 Aのエントリサンクと一緒に、BからBへの出口サンクを排除することは安全です。 また、異なる関数に対して生成された場合でも、同じコードを含むすべての終了サンクとエントリ サンクをエイリアス化しても安全です。

サンクを終了する

前のセクションの関数 fAfB、および fC の例を使用して、コンパイラは次のように fBfC の両方の終了サンクを生成します。

int fB(int a, double b, int i1, int i2, int i3); のサンクを終了する

$iexit_thunk$cdecl$i8$i8di8i8i8:
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         x3,[sp,#0x20]  ; Spill 5th param (i3) into the stack
    fmov        d1,d0          ; Move 2nd param (b) from d0 to XMM1 (x1)
    mov         x3,x2          ; Move 4th param (i2) from x2 to R9 (x3)
    mov         x2,x1          ; Move 3rd param (i1) from x1 to R8 (x2)
    blr         xip0           ; Call the emulator
    mov         x0,x8          ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x10
    ret

サンクを int fC(int a, struct SC c, int i1, int i2, int i3); に終了します。

$iexit_thunk$cdecl$i8$i8m3i8i8i8:
    stp         fp,lr,[sp,#-0x20]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         w1,[sp,#0x40]       ; Spill 2nd param (c) onto the stack
    add         x1,sp,#0x40         ; Make RDX (x1) point to the spilled 2nd param
    str         x4,[sp,#0x20]       ; Spill 5th param (i3) into the stack
    blr         xip0                ; Call the emulator
    mov         x0,x8               ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x20
    ret

fBの場合、double パラメーターが存在すると、残りの GP レジスタの割り当てが再シャッフレされます。これは、Arm64 と x64 の異なる割り当て規則の結果です。 x64 ではレジスタに 4 つのパラメーターしか割り当てないため、5 番目のパラメーターをスタックにスピルする必要があることがわかります。

fCの場合、2 番目のパラメーターは 3 バイト長の構造体です。 Arm64 を使用すると、任意のサイズ構造をレジスタに直接割り当てることができます。 x64 では、サイズ 1、2、4、および 8 のみが許可されます。 この Exit Thunk は、この struct をレジスタからスタックに転送し、代わりにレジスタへのポインターを割り当てる必要があります。 この方法では、(ポインターを運ぶために) 1 つのレジスタが引き続き使用されるため、残りのレジスタの割り当ては変更されません。3 番目と 4 番目のパラメーターに対してレジスタの再シャッフは行われません。 fBの場合と同様に、5 番目のパラメーターをスタックにスピルする必要があります。

終了サンクに関するその他の考慮事項:

  • コンパイラは、変換元や変換先の関数名ではなく、解釈するシグネチャで名前を付けます。 この名前付け規則により、冗長性を簡単に見つけることができます。
  • 呼び出しチェッカーは、ターゲット (x64) 関数のアドレスを伝達するレジスタ x9 を設定します。 Exit Thunk はエミュレーターを呼び出し、 x9 を変更せずに渡します。

パラメーターを再配置した後、Exit Thunk は __os_arm64x_dispatch_call_no_redirectを介してエミュレーターを呼び出します。

この時点で、呼び出しチェッカーとそのカスタム ABI の機能を確認する価値があります。 fBへの間接的な呼び出しは次のようになります。

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, $iexit_thunk$cdecl$i8$i8di8i8i8    ; fB function's exit thunk
add     x10, x10, $iexit_thunk$cdecl$i8$i8di8i8i8
blr     x9                                      ; check target function
blr     x11                                     ; call function

呼び出しチェッカーを呼び出すとき:

  • x11 は、呼び出すターゲット関数のアドレスを提供します (この場合fB )。 この時点で、呼び出しチェッカーはターゲット関数が Arm64EC か x64 かわかりません。
  • x10 は、呼び出される関数のシグネチャに一致する Exit Thunk を提供します (この場合fB )。

呼び出しチェッカーが返すデータは、ターゲット関数が Arm64EC か x64 かによって異なります。

ターゲットが Arm64EC の場合:

  • x11 は、呼び出す Arm64EC コードのアドレスを返します。 この値は、指定されたものと同じ場合があります。

ターゲットが x64 コードの場合:

  • x11 は Exit Thunk のアドレスを返します。 このアドレスは、 x10で指定された入力からコピーされます。
  • x10 は、入力から制御されずに Exit Thunk のアドレスを返します。
  • x9 はターゲット x64 関数を返します。 この値は、 x11経由で指定されたものと同じ場合があります。

呼び出しチェッカーは、常に呼び出し規約パラメーター レジスタを乱されずに残します。 呼び出し元のコードは、 blr x11 (または末尾呼び出しの場合は br x11 ) を使用して、呼び出しチェッカーの呼び出しに直ちに従う必要があります。 呼び出しチェッカーは常に、標準の不揮発性レジスタ ( x0-x8x15(chkstk)、および q0-q7) 以上のレジスタを保持します。

エントリ サンク

エントリ サンクは、x64 から Arm64 呼び出し規則に必要な変換を行います。 この変換は基本的に Exit Thunks の逆ですが、考慮すべきいくつかの側面が含まれます。

fAのコンパイルの前の例を考えてみましょう。 x64 コードが fAを呼び出すことができるように、Entry Thunk が生成されます。

エントリサンク int fA(int a, double b, struct SC c, int i1, int i2, int i3)

$ientry_thunk$cdecl$i8$i8dm3i8i8i8:
    stp         q6,q7,[sp,#-0xA0]!  ; Spill full non-volatile XMM registers
    stp         q8,q9,[sp,#0x20]
    stp         q10,q11,[sp,#0x40]
    stp         q12,q13,[sp,#0x60]
    stp         q14,q15,[sp,#0x80]
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    ldrh        w1,[x2]             ; Load 3rd param (c) bits [15..0] directly into x1
    ldrb        w8,[x2,#2]          ; Load 3rd param (c) bits [16..23] into temp w8
    bfi         w1,w8,#0x10,#8      ; Merge 3rd param (c) bits [16..23] into x1
    mov         x2,x3               ; Move the 4th param (i1) from R9 (x3) to x2
    fmov        d0,d1               ; Move the 2nd param (b) from XMM1 (d1) to d0
    ldp         x3,x4,[x4,#0x20]    ; Load the 5th (i2) and 6th (i3) params
                                    ; from the stack into x3 and x4 (using x4)
    blr         x9                  ; Call the function (fA)
    mov         x8,x0               ; Move the return from x0 to x8 (RAX)
    ldp         fp,lr,[sp],#0x10
    ldp         q14,q15,[sp,#0x80]  ; Restore full non-volatile XMM registers
    ldp         q12,q13,[sp,#0x60]
    ldp         q10,q11,[sp,#0x40]
    ldp         q8,q9,[sp,#0x20]
    ldp         q6,q7,[sp],#0xA0
    adrp        xip0,__os_arm64x_dispatch_ret
    ldr         xip0,[xip0,__os_arm64x_dispatch_ret]
    br          xip0

エミュレーターは、 x9のターゲット関数のアドレスを提供します。

エントリ サンクを呼び出す前に、x64 エミュレーターはスタックから LR レジスタにリターン アドレスをポップします。 LR は、コントロールが Entry Thunk に転送されるときに x64 コードを指すと想定されます。

エミュレーターは、次に応じて、スタックに対して別の調整を実行する場合もあります。Arm64 と x64 の両方の ABIs は、関数が呼び出された時点でスタックを 16 バイトにアラインする必要があるスタック配置要件を定義します。 Arm64 コードを実行すると、ハードウェアによってこの規則が適用されますが、x64 に対するハードウェアの適用はありません。 x64 コードを実行しているとき、アライメントが取れていないスタックで誤って関数を呼び出しても、それに気が付かないままの状態が続く可能性があります。ただし、一部の SSE 命令などが要求する16バイトアラインメント命令を使用するか、Arm64EC コードが呼び出されたときに、問題が顕在化することがあります。

この潜在的な互換性の問題に対処するために、Entry Thunk を呼び出す前に、エミュレーターは常にスタック ポインターを 16 バイトに合わせ、元の値を x4 レジスタに格納します。 これにより、Entry Thunks は常にアラインされたスタックで実行を開始しますが、 x4を介してスタックで渡されたパラメーターを正しく参照できます。

非揮発性 SIMD レジスタに関しては、Arm64 と x64 の呼び出し規則に大きな違いがあります。 Arm64 では、レジスタの下位 8 バイト (64 ビット) は不揮発性と見なされます。 つまり、Dn レジスタのQn部分のみが非揮発性です。 x64 では、 XMMn レジスタの 16 バイト全体が不揮発性と見なされます。 さらに、x64 では、 XMM6 および XMM7 は不揮発性レジスタであるのに対し、D6 および D7 (対応する Arm64 レジスタ) は揮発性です。

これらの SIMD レジスタ操作の非対称性に対処するには、Entry Thunks は、x64 で非揮発性と見なされるすべての SIMD レジスタを明示的に保存する必要があります。 x64 は Arm64 よりも厳密であるため、この節約はエントリ サンクでのみ必要です (Exit Thunks ではありません)。 言い換えると、x64 の登録の保存と保持の規則は、すべてのケースで Arm64 の要件を超えています。

スタックのアンワインド時にこれらのレジスタ値の正しい回復 (setjmp + longjmp、throw + catch など) に対処するために、新しいアンワインド オペコードが導入されました: save_any_reg (0xE7)。 この新しい 3 バイト アンワインド オペコードを使用すると、汎用または SIMD レジスタ (揮発性と見なされるものを含む) を保存し、フルサイズの Qn レジスタを含めます。 この新しいオペコードは、 Qn レジスタの書き込みとフィル操作に使用されます。 save_any_reg は、 save_next_pair (0xE6)と互換性があります。

参考までに、以下のアンワインド情報は、先に示した「エントリ スタブ」に属しています。

   Prolog unwind:
      06: E76689.. +0004 stp   q6,q7,[sp,#-0xA0]! ; Actual=stp   q6,q7,[sp,#-0xA0]!
      05: E6...... +0008 stp   q8,q9,[sp,#0x20]   ; Actual=stp   q8,q9,[sp,#0x20]
      04: E6...... +000C stp   q10,q11,[sp,#0x40] ; Actual=stp   q10,q11,[sp,#0x40]
      03: E6...... +0010 stp   q12,q13,[sp,#0x60] ; Actual=stp   q12,q13,[sp,#0x60]
      02: E6...... +0014 stp   q14,q15,[sp,#0x80] ; Actual=stp   q14,q15,[sp,#0x80]
      01: 81...... +0018 stp   fp,lr,[sp,#-0x10]! ; Actual=stp   fp,lr,[sp,#-0x10]!
      00: E1...... +001C mov   fp,sp              ; Actual=mov   fp,sp
                   +0020 (end sequence)
   Epilog #1 unwind:
      0B: 81...... +0044 ldp   fp,lr,[sp],#0x10   ; Actual=ldp   fp,lr,[sp],#0x10
      0C: E74E88.. +0048 ldp   q14,q15,[sp,#0x80] ; Actual=ldp   q14,q15,[sp,#0x80]
      0F: E74C86.. +004C ldp   q12,q13,[sp,#0x60] ; Actual=ldp   q12,q13,[sp,#0x60]
      12: E74A84.. +0050 ldp   q10,q11,[sp,#0x40] ; Actual=ldp   q10,q11,[sp,#0x40]
      15: E74882.. +0054 ldp   q8,q9,[sp,#0x20]   ; Actual=ldp   q8,q9,[sp,#0x20]
      18: E76689.. +0058 ldp   q6,q7,[sp],#0xA0   ; Actual=ldp   q6,q7,[sp],#0xA0
      1C: E3...... +0060 nop                      ; Actual=90000030
      1D: E3...... +0064 nop                      ; Actual=ldr   xip0,[xip0,#8]
      1E: E4...... +0068 end                      ; Actual=br    xip0
                   +0070 (end sequence)

Arm64EC 関数が戻ると、 __os_arm64x_dispatch_ret ルーチンはエミュレーターを再入力し、x64 コード ( LR が指す) に戻ります。

Arm64EC 関数は、実行時に使用される情報を格納するために、関数の最初の命令の前に 4 バイトを予約します。 これら 4 バイトでは、関数の Entry Thunk の相対アドレスが見つかります。 x64 関数から Arm64EC 関数への呼び出しを実行する場合、エミュレーターは関数の開始前に 4 バイトを読み取り、下位 2 ビットをマスクして、その量を関数のアドレスに追加します。 このプロセスにより、呼び出す Entry Thunk のアドレスが生成されます。

アジャスタ サンクス

Adjustor Thunks は、制御を別の関数に転送 (末尾呼び出し) するシグネチャのない関数です。 制御を転送する前に、パラメーターの 1 つを変換します。 変換されるパラメーターの型は既知ですが、残りのパラメーターはすべて任意の値にすることができ、任意の数にできます。 Adjustor Thunks は、パラメーターを保持する可能性があるレジスタには触れず、スタックに触れられません。 この特性により、Adjustor Thunks はシグネチャを持たない関数になります。

コンパイラは、Adjustor Thunks を自動的に生成できます。 この生成は一般的です。たとえば、C++ の多重継承では、仮想メソッドは、 this ポインターの調整を除き、変更なしで親クラスに委任できます。

次の例は、実際のシナリオを示しています。

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    b           CObjectContext::Release

サンクは、 this ポインターに 8 バイトを減算し、呼び出しを親クラスに転送します。

要約すると、x64 関数から呼び出せる Arm64EC 関数には、Entry Thunk が関連付けられている必要があります。 エントリ サンクはシグネチャ固有です。 Adjustor Thunks などの Arm64 シグネチャのない関数には、シグネチャのない関数を処理できる別のメカニズムが必要です。

Adjustor Thunk の Entry Thunk は、 __os_arm64x_x64_jump ヘルパーを使用して、実際のエントリ サンク作業の実行を延期します (1 つの規則から他方の規則にパラメーターを調整します)。 署名が明らかになるのはこの時点です。 これには、Adjustor Thunk のターゲットが x64 関数であることが判明した場合に、呼び出し規則の調整をまったく行わないオプションが含まれます。 Entry Thunk の実行が開始されるまでに、パラメーターは x64 形式になります。

上記の例では、Arm64EC でのコードの外観を考えてみましょう。

Arm64EC のアジャスタ サンク

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x11,x9,CObjectContext::Release
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    adrp        xip0, __os_arm64x_check_icall
    ldr         xip0,[xip0, __os_arm64x_check_icall]
    blr         xip0
    ldp         fp,lr,[sp],#0x10
    br          x11

アジャスタサンクのエントリトランク

[thunk]:CObjectContext::Release$entry_thunk`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x9,x9,CObjectContext::Release
    adrp        xip0,__os_arm64x_x64_jump
    ldr         xip0,[xip0,__os_arm64x_x64_jump]
    br          xip0

早送りシーケンス

一部のアプリケーションでは、関数が呼び出されたときに実行を迂回する目的で、所有していないバイナリに存在する関数 (通常はオペレーティング システム バイナリ) に依存する関数に対してランタイム変更を行います。 このプロセスはフックとも呼ばれます。

大まかに言うと、フックのプロセスは簡単です。 ただし、フックの詳細はアーキテクチャ固有であり、フック ロジックが対処する必要がある潜在的なバリエーションを考えると非常に複雑です。

一般に、このプロセスには次の手順が含まれます。

  • フックする関数のアドレスを決定します。
  • 関数の最初の命令をフック ルーチンへのジャンプに置き換えます。
  • フックが完了したら、変位元の命令の実行を含む元のロジックに戻ります。

バリエーションは、次のようなものから生じます。

  • 最初の命令のサイズ:他のスレッドが実行中に関数の先頭を置き換えないように、同じサイズまたは小さいJMPに置き換えることをお勧めします。
  • 最初の命令の種類: 最初の命令に PC の相対的な性質がある場合は、再配置するには、変位フィールドなどを変更する必要があります。 命令が離れた場所に移動するとオーバーフローする可能性があるため、この変更では、異なる命令を完全に含む同等のロジックを提供する必要がある場合があります。

このような複雑さのため、堅牢で汎用的なフック ロジックを見つけることはまれです。 多くの場合、アプリケーションに存在するロジックは、アプリケーションが関心のある特定の API で発生すると予想される限られた一連のケースにのみ対処できます。 アプリケーションの互換性の問題の量を想像するのは難しいことではありません。 コードやコンパイラの最適化を簡単に変更しても、コードが想定どおりに表示されなくなった場合は、アプリケーションが使用できなくなる可能性があります。

フックを設定するときに Arm64 コードが発生した場合、これらのアプリケーションはどうなるでしょうか。 彼らは最も確かに失敗します。

高速順方向シーケンス (FFS) 関数は、Arm64EC のこの互換性要件に対処します。

FFS は、実際のロジックを含まない非常に小さな x64 関数であり、実際の Arm64EC 関数への末尾呼び出しです。 これらは省略可能ですが、すべての DLL エクスポートと、 __declspec(hybrid_patchable)で修飾された任意の関数に対して既定で有効になります。

このような場合、エクスポート ケースでGetProcAddressするか、&functionのケースで__declspec(hybrid_patchable)することによって、コードが特定の関数へのポインターを取得すると、結果のアドレスには x64 コードが含まれます。 そのx64コードは正当なx64関数に渡され、現在利用可能なフックロジックの大部分を満たす。

次の例を考えてみましょう (簡潔にするためにエラー処理は省略されています)。

auto module_handle = 
    GetModuleHandleW(L"api-ms-win-core-processthreads-l1-1-7.dll");

auto pgma = 
    (decltype(&GetMachineTypeAttributes))
        GetProcAddress(module_handle, "GetMachineTypeAttributes");

hr = (*pgma)(IMAGE_FILE_MACHINE_Arm64, &MachineAttributes);

pgma変数の関数ポインター値には、GetMachineTypeAttributesの FFS のアドレスが含まれています。

この例では、Fast-Forward シーケンスを示します。

kernelbase!EXP+#GetMachineTypeAttributes:
00000001`800034e0 488bc4          mov     rax,rsp
00000001`800034e3 48895820        mov     qword ptr [rax+20h],rbx
00000001`800034e7 55              push    rbp
00000001`800034e8 5d              pop     rbp
00000001`800034e9 e922032400      jmp     00000001`80243810

FFS x64 関数には正規のプロローグとエピローグがあり、Arm64EC コードの実際の GetMachineTypeAttributes 関数への末尾呼び出し (ジャンプ) で終わる。

kernelbase!GetMachineTypeAttributes:
00000001`80243810 d503237f pacibsp
00000001`80243814 a9bc7bfd stp         fp,lr,[sp,#-0x40]!
00000001`80243818 a90153f3 stp         x19,x20,[sp,#0x10]
00000001`8024381c a9025bf5 stp         x21,x22,[sp,#0x20]
00000001`80243820 f9001bf9 str         x25,[sp,#0x30]
00000001`80243824 910003fd mov         fp,sp
00000001`80243828 97fbe65e bl          kernelbase!#__security_push_cookie
00000001`8024382c d10083ff sub         sp,sp,#0x20
                           [...]

2 つの Arm64EC 関数間で 5 つのエミュレートされた x64 命令を実行する必要がある場合は、非常に非効率的です。 FFS 関数は特殊です。 FFS 関数は、変更されていない場合は実際には実行されません。 呼び出しチェッカー ヘルパーは、FFS が変更されていないかどうかを効率的にチェックします。 その場合、通話は実際の宛先に直接転送されます。 FFS が可能な方法で変更された場合、FFS ではなくなります。 実行は、変更された FFS に転送され、存在する可能性がある任意のコードを実行し、迂回とフック ロジックをエミュレートします。

フックが FFS の末尾に実行を戻すと、最終的に Arm64EC コードの末尾呼び出しに達します。これは、アプリケーションが期待するとおりに、フックの後に実行されます。

アセンブリでの Arm64EC の記述

Windows SDK ヘッダーと C コンパイラにより、Arm64EC アセンブリの作成作業が簡略化されます。 たとえば、C コンパイラを使用して、C コードからコンパイルされていない関数のエントリサンクと終了サンクを生成できます。

アセンブリ (ASM) で作成する必要がある次の関数 fD と同等の例を考えてみましょう。 Arm64EC コードと x64 コードはどちらもこの関数を呼び出すことができます。 pfE 関数ポインターは、Arm64EC または x64 コードを指すことができます。

typedef int (PF_E)(int, double);

extern PF_E * pfE;

int fD(int i, double d) {
    return (*pfE)(i, d);
}

ASM で fD を記述すると、次のコードのようになります。

#include "ksarm64.h"

        IMPORT  __os_arm64x_check_icall_cfg
        IMPORT |$iexit_thunk$cdecl$i8$i8d|
        IMPORT pfE

        NESTED_ENTRY_COMDAT A64NAME(fD)
        PROLOG_SAVE_REG_PAIR fp, lr, #-16!

        adrp    x11, pfE                                  ; Get the global function
        ldr     x11, [x11, pfE]                           ; pointer pfE

        adrp    x9, __os_arm64x_check_icall_cfg           ; Get the EC call checker
        ldr     x9, [x9, __os_arm64x_check_icall_cfg]     ; with CFG
        adrp    x10, |$iexit_thunk$cdecl$i8$i8d|          ; Get the Exit Thunk for
        add     x10, x10, |$iexit_thunk$cdecl$i8$i8d|     ; int f(int, double);
        blr     x9                                        ; Invoke the call checker

        blr     x11                                       ; Invoke the function

        EPILOG_RESTORE_REG_PAIR fp, lr, #16!
        EPILOG_RETURN

        NESTED_END

        end

前の例の場合:

  • Arm64EC では、Arm64 と同じプロシージャ宣言とプロローグ/エピローグ マクロが使用されます。
  • 関数名を A64NAME マクロでラップします。 C または C++ コードを Arm64EC としてコンパイルすると、コンパイラによって、 OBJ が Arm64EC コードを含む ARM64EC としてマークされます。 このマーキングは、 ARMASMでは発生しません。 ASM コードをコンパイルするときに、関数名の前に # を付けることで、生成されたコードが Arm64EC であることをリンカーに通知できます。 A64NAME マクロは、_ARM64EC_が定義されているときにこの操作を実行し、_ARM64EC_が定義されていない場合は名前を変更しません。 この方法により、Arm64 と Arm64EC の間でソース コードを共有できます。
  • ターゲット関数が x64 の場合は、まず、EC 呼び出しチェッカーを介して pfE 関数ポインターを適切な終了サンクと共に実行する必要があります。

エントリーおよびエグジットサンクの生成

次の手順では、 fD のエントリ サンクと、 pfEの終了サンクを生成します。 C コンパイラは、 _Arm64XGenerateThunk コンパイラ キーワードを使用して、最小限の労力でこのタスクを実行できます。

void _Arm64XGenerateThunk(int);

int fD2(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(2);
    return 0;
}

int fE(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(1);
    return 0;
}

_Arm64XGenerateThunk キーワードは、C コンパイラに関数シグネチャの使用、本文の無視、終了サンク (パラメーターが 1 の場合) またはエントリ サンク (パラメーターが 2 の場合) のいずれかを生成するように指示します。

サンク生成を専用のCファイルに配置します。 分離ファイルを使用すると、対応する OBJ シンボルをダンプしたり、逆アセンブルしたりすることで、シンボル名を簡単に確認できます。

カスタムエントリサンク (エントリポイントでの処理の遅延評価)

SDK には、手動でコード化されたカスタム エントリ サンクを作成するのに役立つマクロが含まれています。 これらのマクロは、カスタムアジャスターサンクを作成する際に使用できます。

ほとんどのアジャスタ サンク (adjustor thunk) は C++ コンパイラーによって生成されますが、手動で生成することも可能です。 ジェネリック コールバックが制御を実際のコールバックに移譲し、パラメーターの 1 つがその実際のコールバックを識別する場合は、アジャスター サンクを手動で生成することがあります。

次の例は、Arm64 Classic コードにおけるアジャスタースタンクを示しています。

    NESTED_ENTRY MyAdjustorThunk
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x15, [x0, 0x18]
    adrp    x16, __guard_check_icall_fptr
    ldr     x16, [x16, __guard_check_icall_fptr]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x15
    NESTED_END

この例では、最初のパラメーターは構造体への参照を提供します。 このコードは、この構造体の要素からターゲット関数アドレスを取得します。 構造は書き込み可能であるため、制御フロー ガード (CFG) はターゲット アドレスを検証する必要があります。

次の例は、同等の adjustor thunk を Arm64EC に移植する手順を示しています。

    NESTED_ENTRY_COMDAT A64NAME(MyAdjustorThunk)
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x11, [x0, 0x18]
    adrp    xip0, __os_arm64x_check_icall_cfg
    ldr     xip0, [xip0, __os_arm64x_check_icall_cfg]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x11
    NESTED_END

上記のコードでは、終了サンク (レジスタ x10 内) は提供されません。 コードはさまざまなシグネチャに対して実行できるため、この方法は不可能です。 このコードは、呼び出し元がx10を終了処理に設定することを利用しています。 呼び出し元は、明示的な署名を対象とする呼び出しを行います。

上記のコードでは、呼び出し元が x64 コードの場合に対処するためのエントリ サンクが必要です。 次の例は、カスタムエントリサンク用マクロを使用して、対応するエントリサンクを作成する方法を示しています。

    ARM64EC_CUSTOM_ENTRY_THUNK A64NAME(MyAdjustorThunk)
    ldr     x9, [x0, 0x18]
    adrp    xip0, __os_arm64x_x64_jump
    ldr     xip0, [xip0, __os_arm64x_x64_jump]
    br      xip0
    LEAF_END

他の関数とは異なり、このエントリサンクは最終的に関連する関数(アジャスタサンク)に制御を転送しません。 この場合、エントリ サンクは機能自体を埋め込み (パラメーター調整を実行)、 __os_arm64x_x64_jump ヘルパーを介して制御をエンド ターゲットに直接転送します。

Arm64EC コードの動的生成 (JIT コンパイル)

Arm64EC プロセスでは、Arm64EC コードと x64 コードの 2 種類の実行可能メモリが存在します。

オペレーティング システムは、読み込まれたバイナリからこの情報を抽出します。 x64 バイナリはすべて x64 であり、Arm64EC バイナリには Arm64EC と x64 コード ページの範囲テーブルが含まれています。

動的に生成されたコードはどうですか? Just-In-Time (JIT) コンパイラは実行時に、バイナリ ファイルによってサポートされていないコードを生成します。

通常、このプロセスには次の手順が含まれます。

  • 書き込み可能なメモリの割り当て (VirtualAlloc)。
  • 割り当てられたメモリ内のコードを生成します。
  • メモリを読み書きから読み取り/実行 (VirtualProtect) に再度保護します。
  • 非自明 (非リーフ) で生成されたすべての関数 (RtlAddFunctionTable または RtlAddGrowableFunctionTable) に対してアンワインド用の関数エントリを追加します。

互換性上の簡単な理由から、アプリケーションが Arm64EC プロセスでこれらの手順を実行すると、オペレーティング システムはコードを x64 コードと見なします。 この動作は、変更されていない x64 Java ランタイム、.NET ランタイム、JavaScript エンジンなどを使用するすべてのプロセスで発生します。

Arm64EC 動的コードを生成するには、同じプロセスに従って 2 つの違いがあります。

  • メモリを割り当てるときは、(VirtualAlloc2VirtualAllocではなく) 新しいVirtualAllocExを使用し、MEM_EXTENDED_PARAMETER_EC_CODE属性を指定します。
  • 関数エントリを追加する場合:
    • これらは Arm64 形式である必要があります。 Arm64EC コードをコンパイルする場合、 RUNTIME_FUNCTION 型は x64 形式と一致します。 Arm64EC のコンパイル時に Arm64 形式の場合は、代わりに ARM64_RUNTIME_FUNCTION 型を使用します。
    • 古い RtlAddFunctionTable API は使用しないでください。 常に新しい RtlAddGrowableFunctionTable API を使用します。

メモリ割り当ての例を次に示します。

    MEM_EXTENDED_PARAMETER Parameter = { 0 };
    Parameter.Type = MemExtendedParameterAttributeFlags;
    Parameter.ULong64 = MEM_EXTENDED_PARAMETER_EC_CODE;

    HANDLE process = GetCurrentProcess();
    ULONG allocationType = MEM_RESERVE;
    DWORD protection = PAGE_EXECUTE_READ | PAGE_TARGETS_INVALID;

    address = VirtualAlloc2 (
        process,
        NULL,
        numBytesToAllocate,
        allocationType,
        protection,
        &Parameter,
        1);

次の例は、アンワインド関数エントリを 1 つ追加する方法を示しています。

ARM64_RUNTIME_FUNCTION FunctionTable[1];

FunctionTable[0].BeginAddress = 0;
FunctionTable[0].Flags = PdataPackedUnwindFunction;
FunctionTable[0].FunctionLength = nSize / 4;
FunctionTable[0].RegF = 0;                   // no D regs saved
FunctionTable[0].RegI = 0;                   // no X regs saved beyond fp,lr
FunctionTable[0].H = 0;                      // no home for x0-x7
FunctionTable[0].CR = PdataCrChained;        // stp fp,lr,[sp,#-0x10]!
                                             // mov fp,sp
FunctionTable[0].FrameSize = 1;              // 16 / 16 = 1

this->DynamicTable = NULL;
Result == RtlAddGrowableFunctionTable(
    &this->DynamicTable,
    reinterpret_cast<PRUNTIME_FUNCTION>(FunctionTable),
    1,
    1,
    reinterpret_cast<ULONG_PTR>(pBegin),
    reinterpret_cast<ULONG_PTR>(reinterpret_cast<PBYTE>(pBegin) + nSize)
);