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

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

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

Arm64EC の設計

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

Arm64EC は、主に従来の Arm64 ABI に対する添加剤です。 クラシック 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 の状態を定義します。 これは、 や GetThreadContext API などにException Handling使用されます。 既存の x64 コードでは、CPU コンテキストが x64 構造体として表されるか、つまり、CONTEXTx64 CONTEXT コンパイル時に定義される構造体として表されることを想定しています。

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

また、x64 に取り付けることができない Arm64 CONTEXT レジスタは、使用する操作 CONTEXT が発生するたびに値が失われる可能性があるため、使用しないでください (マネージド言語ランタイムのガベージ コレクション操作や APC など、非同期で予期しないものもあります)。

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

たとえば、 RCX は にマップされ RDXX0、 は にX1RSPマップされ、 は SPRIPPCマップされます。また、レジスタ x13、、x14x23v31x24x28v16-、 に表現がないため、Arm64EC で使用できない方法も確認できます。

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

通話チェッカー

呼び出しチェッカーは、Windows 8.1で制御フロー ガード (CFG) が導入されて以来、Windows の一部でした。 呼び出しチェッカーは、関数ポインターのアドレスサニタイザーです (これらの処理がアドレスサニタイザーと呼ばれる前)。 オプション /guard:cf を使用してコードがコンパイルされるたびに、コンパイラは間接呼び出し/ジャンプの直前にチェッカー関数への追加の呼び出しを生成します。 チェッカー関数自体は Windows によって提供され、CFG の場合、既知の適切な呼び出し先に対して有効性チェックを実行します。 この情報は、 でコンパイルされた /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 とのわずかな違いは次のとおりです。

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

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

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

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

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

Binary コード 保護されていない間接呼び出し 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 は通常、 関数のプロローグから呼び出されます。 そのため、最適なコード生成のために、カスタム呼び出し規則が使用されます。

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

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

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

上記で__chkstk説明した内容はすべて、 とそれに対応する __security_check_cookie Arm64EC に等しく適用されます。 __security_check_cookie_arm64ec

可変個引数呼び出し規則

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

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

  • パラメーターの受け渡しには、最初の 4 つのレジスタのみが使用されます: x0x1x2x3。 残りのパラメーターはスタックにスピルされます。 これは x64 可変個引数呼び出し規則に正確に従っており、レジスタx0>x7が使用される Arm64 クラシックとは異なります。
  • レジスタによって渡される浮動小数点/SIMD パラメーターは、SIMD ではなく、General-Purpose レジスタを使用します。 これは Arm64 クラシックに似ていますが、FP/SIMD パラメーターが General-Purpose レジスタと SIMD レジスタの両方で渡される x64 とは異なります。 たとえば、x64 で としてf1(int, double)呼び出される関数f1(int, …)の場合、2 番目のパラメーターは と XMM1の両方RDXに割り当てられます。 Arm64EC では、2 番目のパラメーターは にだけ x1割り当てられます。
  • レジスタを介して値によって構造体を渡す場合、x64 サイズルールが適用されます。サイズが正確に 1、2、4、8 の構造体は、General-Purpose レジスタに直接読み込まれます。 他のサイズの構造体はスタックにスピルされ、スピルされた場所へのポインターがレジスタに割り当てられます。 これは基本的に、低レベルで、by-value を参照ごとに降格します。 クラシック Arm64 ABI では、最大 16 バイトの任意のサイズの構造体がGeneral-Purposedレジスタに直接割り当てられます。
  • 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 の呼び出し規則の違いを処理します。

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

開始と終了サンク

Entry および Exit Thunks では、Arm64EC 呼び出し規則 (従来の Arm64 とほとんど同じ) を x64 呼び出し規則に変換します。その逆も同様です。

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

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

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

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

  • Arm64: a -> x0、b -> x1、c -> x2、d -> x3
  • x64: a -> 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: a -> RCX、b -> XMM1、c -> R8、d -> XMM3
  • Arm64 -> x64 変換: x0 -> RCX、d0 -> XMM1、x1 -> R8、d1 -> XMM3

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

この理由から、エントリサンクと Exit サンクは存在し、個々の関数シグネチャに合わせて特別に調整されています。

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

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

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

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' のサンクを終了する

エントリ サン fA クは、ケース fA で生成され、x64 コードから呼び出されます。 と のfBfC終了サンクは、ケースやfC、x64 コードであることが判明した場合fBに生成されます。

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

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

サンクを終了する

上記の関数fAfBfCの例を使用すると、コンパイラが と fC Exit Thunks の両方fBを生成する方法を次に示します。

サンクを終了して 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 番目のパラメーターをスタックにスピルする必要があります。

Exit Thunks に関するその他の考慮事項:

  • コンパイラは、変換元の関数名>ではなく、アドレス指定する署名で名前を付けます。 これにより、冗長性を簡単に見つけることができます。
  • Exit Thunk は、ターゲット (x64) 関数のアドレスを保持するレジスタ x9 で呼び出されます。 これは呼び出しチェッカーによって設定され、Exit Thunk (妨がれず) をエミュレーターに渡します。

パラメーターを再配置した後、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 、末尾呼び出しの場合)。 これらはレジスタ呼び出しチェッカーです。 これらは常に、、(chkstk)-q7q0 および の標準の非揮発性レジスタをx15x0-x8超えて保持されます。

エントリ サンク

エントリ サンクは、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によって提供されます。

Entry Thunk を呼び出す前に、x64 エミュレーターはスタックからレジスタにリターン アドレスを LR ポップします。 その後、コントロールが LR Entry Thunk に転送されるときに x64 コードを指している必要があります。

エミュレーターは、次に応じて、スタックに対して別の調整を実行することもできます。Arm64 と x64 の両方の AVI では、関数が呼び出される時点でスタックを 16 バイトに配置する必要があるスタック配置要件が定義されています。 Arm64 コードを実行する場合、ハードウェアはこの規則を適用しますが、x64 に対するハードウェアの適用はありません。 x64 コードの実行中、一部の 16 バイトアラインメント命令が使用されるか (一部の SSE 命令で実行される)、または Arm64EC コードが呼び出されるまで、整列されていないスタックを使用して関数を誤って呼び出すと、無期限に気付かないことがあります。

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

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

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

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

参考までに、上記の Entry Thunk に属する対応するアンワインド情報を次に示します。

   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 バイトがあります。 関数の Entry Thunk の相対アドレスが見つかるのは、この 4 バイトです。 x64 関数から Arm64EC 関数への呼び出しを実行すると、エミュレーターは関数の開始前に 4 バイトを読み取り、下位 2 つのビットをマスクアウトし、その量を関数のアドレスに追加します。 これにより、呼び出すエントリ サンクのアドレスが生成されます。

アジャスタ サンク

Adjustor Thunks は、いずれかのパラメーターへの変換を実行した後、単に別の関数に制御を転送 (末尾呼び出し) するシグネチャレス関数です。 変換されるパラメーターの型は既知ですが、残りのすべてのパラメーターは何でもかまいません。また、任意の数の Adjustor Thunks は、パラメーターを保持している可能性のあるレジスタに触れず、スタックに触れることはありません。 これは、Adjustor Thunks のシグネチャレス関数を作るものです。

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

実際の例を次に示します。

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

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

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

Adjustor Thunk の Entry Thunk は、ヘルパーを __os_arm64x_x64_jump 使用して、実際のエントリ サンク作業の実行を延期します (一方の規則から他方の規則にパラメーターを調整します)。 この時点で、署名が明らかになります。 これには、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

Fast-Forward シーケンス

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

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

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

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

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

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

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

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

Fast-Forward シーケンス (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 関数ポインター値には、 の FFS の GetMachineTypeAttributesアドレスが含まれます。

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 コードからコンパイルされない関数の Entry と Exit Thunks を生成できます。

Assembly (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 としてコンパイルする場合、コンパイラは を Arm64EC コードを含んでいるものとしてArm64ECマークOBJします。 これは では発生 ArmASMしません。 ASM コードをコンパイルするときに、生成されたコードが Arm64EC であることをリンカーに通知する別の方法があります。 これは、関数名の前に を付けることで行われます #。 マクロは A64NAME 、 が定義されている場合 _Arm64EC_ にこの操作を実行し、 が定義されていない場合 _Arm64EC_ は名前を変更しません。 これにより、Arm64 と Arm64EC の間でソース コードを共有できます。
  • 関数ポインターは pfE 、ターゲット関数が x64 の場合に、まず EC 呼び出しチェッカーと適切な Exit Thunk を介して実行する必要があります。

エントリサンクと終了サンクの生成

次の手順では、 のエントリ サンク fD と の Exit Thunk を生成します 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 、関数シグネチャを使用し、本文を無視し、Exit Thunk (パラメーターが 1 の場合) または Entry Thunk (パラメーターが 2 の場合) のいずれかを生成するように C コンパイラに指示します。

サンク生成を独自の C ファイルに配置することをお勧めします。 分離ファイル内に存在すると、対応する OBJ シンボルをダンプしたり、逆アセンブリを行ったりして、シンボル名を簡単に確認できます。

カスタム エントリ サンク

カスタムの手動でコード化された Entry Thunks の作成に役立つマクロが SDK に追加されました。 これを使用できる 1 つのケースは、カスタム Adjustor Thunks を作成する場合です。

ほとんどの Adjustor サンクは C++ コンパイラによって生成されますが、手動で生成することもできます。 これは、ジェネリック コールバックが、いずれかのパラメーターによって識別される実際のコールバックに制御を転送する場合に見つかります。

Arm64 クラシック コードの例を次に示します。

    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

この例では、ターゲット関数のアドレスは、参照によって提供される構造体の要素から 1 番目のパラメーターを介して取得されます。 構造体は書き込み可能であるため、ターゲット アドレスは制御フロー ガード (CFG) を使用して検証する必要があります。

次の例は、Arm64EC に移植した場合の同等の Adjustor Thunk の外観を示しています。

    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

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

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

    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

他の関数とは異なり、この Entry Thunk は最終的に関連する関数 (Adjustor Thunk) に制御を転送しません。 この場合、機能自体 (パラメーター調整を実行) は Entry Thunk に埋め込まれており、コントロールはヘルパーを介して __os_arm64x_x64_jump エンド ターゲットに直接転送されます。

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

Arm64EC プロセスには、Arm64EC コードと x64 コードの 2 種類の実行可能メモリがあります。

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

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

通常、これは次のことを意味します。

  • 書き込み可能なメモリの割り当て (VirtualAlloc)。
  • 割り当てられたメモリにコードを生成する。
  • メモリをRead-WriteからRead-Execute (VirtualProtect) に再保護します。
  • 単純でない (リーフ以外の) 生成されたすべての関数 (RtlAddFunctionTable または RtlAddGrowableFunctionTable) にアンワインド関数エントリを追加します。

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

Arm64EC 動的コードを生成する場合、プロセスはほぼ同じですが、違いは 2 つだけです。

  • メモリを割り当てるときは、 または VirtualAllocExではなく新しい VirtualAllocVirtualAlloc2 を使用し、 属性を指定しますMEM_EXTENDED_PARAMETER_EC_CODE
  • 関数エントリを追加する場合:
    • これらは Arm64 形式である必要があります。 Arm64EC コードをコンパイルする場合、 RUNTIME_FUNCTION 型は x64 形式と一致します。 Am64EC をコンパイルするときに 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)
);