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 はほとんど変更されませんでしたが、x64 の相互運用性を実現するために一部が追加されました。

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

Arm64EC と Arm64 クラシック ABI

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

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

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

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

つまり、 _M_AMD64_AMD64_ 定義されます。 この規則の影響を受ける型の 1 つが構造体です CONTEXT 。 この構造体は CONTEXT 、特定の時点での CPU の状態を定義します。 これは、API などにException HandlingGetThreadContext使用されます。 既存の 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 toX0RDX toX1RIPRSPSPPCto 、などにマップされます。また、レジスタ x13、、、、-x23v31x24v16x28、表現がないため、Arm64EC で使用できない方法も確認できます。 x14

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

チェックers を呼び出す

Call チェックers は、Windows 8.1 で Control Flow Guard (CFG) が導入されて以来、これまで Windows の一部でした。 呼び出しチェックerは、関数ポインターのアドレスサニタイザーです (これらの処理がアドレスサニタイザーと呼ばれる前)。 オプション/guard:cfを使用してコードがコンパイルされるたびに、コンパイラは、すべての間接呼び出し/ジャンプの直前に、チェックer 関数への追加の呼び出しを生成します。 チェックer 関数自体は 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 の場合、ターゲットが有効な場合は呼び出しチェックer は単に戻り、そうでない場合はプロセスを高速に失敗します。 呼び出しチェックの呼び出し元には、カスタム呼び出し規則があります。 通常の呼び出し規約で使用されていないレジスタ内の関数ポインターを受け取り、すべての通常の呼び出し規約レジスタを保持します。 この方法では、レジスタの流出は発生しません。

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

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

Arm64EC では、同じ呼び出しチェックer の使用は次のようになります。

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 とのわずかな違いは次のとおりです。

  • 呼び出しチェックer のシンボル名が異なります。
  • ターゲット アドレスは、次の代わりにx15指定されますx11
  • ターゲット アドレス (x11) は [in, out][in].
  • "Exit Thunk" と呼ばれる追加のパラメーター x10があります。

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

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

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

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

バイナリ コード 保護されていない間接呼び出し 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 呼び出しチェックer を参照するコード) は、実行時の CFG 保護を意味しません。 CFG で保護されたバイナリは、CFG をサポートしていないシステムでダウンレベルで実行できます。呼び出しチェックer はコンパイル時に no-op ヘルパーで初期化されます。 プロセスでは、構成によって CFG が無効になっている場合もあります。 以前の AVI で CFG が無効になっている (または OS のサポートが存在しない) 場合、OS はバイナリの読み込み時に呼び出しチェックer を更新しません。 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 は同じシンボル名前空間を共有するため、2 つの関数を指定 __chkstkすることはできません。 既存の x64 コードとの互換性に対応するために、__chkstk名前は x64 スタック チェックer に関連付けられます。 代わりに Arm64EC コードが使用 __chkstk_arm64ec されます。

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

上記で__chkstk述べたすべてが同様に適用され、その Arm64EC に__security_check_cookie対応します。 __security_check_cookie_arm64ec

可変個引数呼び出し規則

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

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

  • パラメーターの受け渡しには、最初の 4 つのレジスタのみが使用されます。 x0x1x2x3 再メインパラメーターはスタックにスピルされます。 これは x64 可変呼び出し規則に厳密に従っており、レジスタx0>x7が使用される Arm64 クラシックとは異なります。
  • レジスタによって渡される浮動小数点/SIMD パラメーターは、SIMD ではなく汎用レジスタを使用します。 これは Arm64 クラシックに似ていますが、汎用レジスタと SIMD レジスタの両方で FP/SIMD パラメーターが渡される x64 とは異なります。 たとえば、x64 で呼び出f1(int, double)される関数f1(int, …)の場合、2 番目のパラメーターは両方RDXXMM1割り当てられます。 Arm64EC では、2 番目のパラメーターは just 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 は異なります。 その結果、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 に入ります )。 Arm64EC 関数が x64 関数を呼び出すと、Exit Thunks は呼び出しチェックer によって自動的に呼び出されます (実行は Arm64EC を終了します)。

Arm64EC コードをコンパイルすると、コンパイラは、そのシグネチャと一致する各 Arm64EC 関数の Entry Thunk を生成します。 コンパイラは、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 コードから呼び出されます。 Exit Thunks for fB and fC are generated in case fB or or fC turn to be x64 code.

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

たとえば、Arm64EC 関数が Arm64EC B関数Aを呼び出すバイナリでは、Bエクスポートされず、そのアドレスが .A エントリサンクと一緒に、出口サンクABを除去することはB安全です. また、異なる関数に対して生成された場合でも、同じコードを含むすべての Exit サンクと Entry サンクをエイリアス化しても安全です。

サンクス出口

上記の例の関数 fBfAfCを使用すると、コンパイラが両方と 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" パラメーターが存在すると、ARM64 と x64 の異なる割り当て規則の結果として、再メイン GP レジスタの割り当てが再シャッフリングされることがわかります。 また、x64 がレジスタに割り当てるパラメーターは 4 つだけなので、5 番目のパラメーターをスタックにスピルする必要があります。

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

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

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

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

この時点で、呼び出しチェックer の機能と、独自のカスタム 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

呼び出しチェックer を呼び出すとき:

  • x11 は、呼び出すターゲット関数のアドレスを提供します (fB この場合)。 ターゲット関数が Arm64EC または x64 の場合、この時点では認識されない可能性があります。
  • x10 は、呼び出される関数のシグネチャに一致する Exit Thunk を提供します (fB この場合)。

呼び出しチェックer によって返されるデータは、ターゲット関数が Arm64EC または x64 であるかによって異なります。

ターゲットが Arm64EC の場合:

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

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

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

呼び出しチェックの場合、呼び出し規約パラメーター レジスタは常に妨がれないため、呼び出し元のコードは、呼び出し元の呼び出しチェック呼び出しに直ちにblr x11従う必要があります (またはbr x11末尾呼び出しの場合)。 これらはレジスタ呼び出しチェックer です。 これらは常に、標準の不揮発性レジスタの上とそれ以降を保持します: x0-x8, x15(chkstk) と .q0-q7

エントリ サンク

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

コンパイル 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 エントリ サンクに転送されるときに、x64 コードを指すと予想されます。

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

この潜在的な互換性の問題に対処するために、Entry Thunk を呼び出す前に、エミュレーターは常にスタック ポインターを 16 バイトにアラインダウンし、元の値をレジスタに x4 格納します。 このようにして、エントリサンクは常に整列されたスタックで実行を開始しますが、 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 バイト アンワインド オペコードを使用すると、汎用または 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 バイトがあります。 関数の Entry Thunk の相対アドレスが見つかるのは、この 4 バイトです。 x64 関数から Arm64EC 関数への呼び出しを実行すると、エミュレーターは関数の開始前に 4 バイトを読み取り、下位 2 ビットをマスクアウトし、その量を関数のアドレスに追加します。 これにより、呼び出すエントリ サンクのアドレスが生成されます。

アジャスタ サンクス

Adjustor Thunks はシグネチャのない関数であり、いずれかのパラメーターに変換を実行した後、単に制御を別の関数に転送 (末尾呼び出し) します。 変換されるパラメーターの型は既知ですが、すべての reメインing パラメーターは何でもかまいません。また、任意の数の場合、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 シグネチャのない関数には、シグネチャのない関数を処理できる別のメカニズムが必要です。

アジャスターサンクのエントリサンクは、ヘルパーを __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

早送りシーケンス

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

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

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

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

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

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

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

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

Fast-Forward Sequence (FFS) 関数は、Arm64EC のこの互換性要件に対処します。

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

このような場合、コードがエクスポートケースまたは&function__declspec(hybrid_patchable)ケースによってGetProcAddress特定の関数へのポインターを取得すると、結果のアドレスには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 関数ポインター値には、's FFS' の GetMachineTypeAttributesアドレスが含まれます。

高速順方向シーケンスの例を次に示します。

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 呼び出し チェックer と適切な Exit Thunk を介して関数ポインターを実行する必要があります。

入退出サンクの生成

次の手順では、エントリ サンクを生成し、終了サンクを fD 生成します pfE。 C コンパイラは、コンパイラ キーワード (keyword)を使用して、最小限の労力でこのタスクを_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;
}

キーワード (keyword)は_Arm64XGenerateThunk、関数シグネチャを使用し、本文を無視して、Exit Thunk (パラメーターが 1 の場合) または Entry Thunk (パラメーターが 2 の場合) を生成するように C コンパイラに指示します。

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

カスタム エントリサンク

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

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

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 が必要です。 カスタム エントリ サンクのマクロを使用して、対応するエントリ サンクを作成する方法を次に示します。

    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 に埋め込まれており、コントロールはヘルパーを介して __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 つだけです。

  • メモリを割り当てるときは、新しい VirtualAlloc2 (またはVirtualAllocEx) 代わりにVirtualAlloc使用し、属性を指定します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)
);