ARM64 例外処理

ARM64 版 Windows は、ハードウェアで生成される非同期例外とソフトウェアで生成される同期例外に対して、同じ構造化例外処理メカニズムを使用します。 言語固有の例外ハンドラーは、言語ヘルパー関数を使用することで、Windows 構造化例外処理に付加して構築します。 このドキュメントでは、ARM64 上の Windows での例外処理について説明します。 Microsoft ARM アセンブラーと MSVC コンパイラによって生成されるコードで使用される言語ヘルパーを示します。

目的と動機

例外アンワインド データ規則およびこの記述は、次のことを目的としています。

  • どのような場合でも、コード プローブを使用しないアンワインドができるように十分な記述を提供します。

    • コードを分析するには、コードをページインする必要があります。 これにより、便利な状況 (トレース、サンプリング、デバッグ) でのアンワインドが防止されます。

    • コードの分析は複雑です。コンパイラは、アンワインダーがデコードできる命令だけを生成するように注意する必要があります。

    • アンワインド コードを使用してアンワインドを完全に記述できない場合は、場合によっては命令デコードにフォールバックする必要があります。 命令デコードにより全体的な複雑さが増し、理想的には回避する必要があります。

  • プロローグ中およびエピローグ中のアンワインドをサポートします。

    • アンワインドは Windows で例外処理以外の目的でも使用されます。 プロローグまたはエピローグのコード シーケンスの途中であっても、コードを正確にアンワインドできることが重要です。
  • 使用する容量を最小限にします。

    • バイナリ サイズが大幅に増加するようなアンワインド コードの集約はしないでください。

    • アンワインド コードはメモリ内でロックされる可能性が高いため、フットプリントを小さくすると、読み込まれる各バイナリのオーバーヘッドが最小限に抑えられます。

前提条件

例外処理の記述では、次のことが前提となっています。

  • 通常、プロローグとエピローグは、相互にミラー化されます。 この共通の特徴を利用することで、アンワインドを記述するために必要なメタデータのサイズを大幅に削減できます。 関数本体内で、プロローグの演算が元に戻されるか、先に進んでエピローグの演算が実行されるかは関係ありません。 両方の場合で同じ結果となります。

  • 関数は全体として比較的小さい傾向があります。 領域の最適化のいくつかは、データの最も効率的なパッキングを実現するために、この事実に依存しています。

  • エピローグに条件付きコードはありません。

  • 専用フレーム ポインター レジスタ: プロローグ内の別のレジスタ (x29) に保存されている場合sp、そのレジスタはメイン関数全体で変更されません。 これは、元のデータ sp をいつでも回復できるということです。

  • 別の sp レジスタに保存されていない限り、スタック ポインターのすべての操作はプロローグとエピローグ内で厳密に行われます。

  • スタック フレームのレイアウトは、次のセクションで説明するように構成されています。

ARM64 のスタック フレームのレイアウト

Diagram that shows the stack frame layout for functions.

フレームチェーン関数の場合、最適化の fp 考慮事項に応じて、ローカル変数領域内の任意の位置に、および lr ペアを保存できます。 目標は、フレーム ポインター () またはスタック ポインター (x29sp) に基づいて、1 つの命令で到達できるローカルの数を最大化することです。 ただし、関数の場合 alloca は、チェーンする必要があり x29 、スタックの一番下を指す必要があります。 レジスタ ペア アドレス指定モードのカバレッジを向上させるために、不揮発性レジスタの保存領域がローカル領域スタックの最上位に配置されます。 ここでは、最も効率的なプロローグ シーケンスの例をいくつか紹介します。 わかりやすくするため、およびキャッシュの局所性を向上させるために、すべての標準プロローグでの、呼び出し先保存レジスタを格納する順序は "拡大" 順です。 #framesz 以下はスタック全体のサイズを表します (面積を除く alloca )。 #localsz#outsz は、それぞれローカル領域のサイズ (<x29, lr> ペアの保存領域を含む)、および出力パラメーターのサイズを表します。

  1. チェーン、#localsz <= 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        stp    x29,lr,[sp,#-localsz]!   // save <x29,lr> at bottom of local area
        mov    x29,sp                   // x29 points to bottom of local
        sub    sp,sp,#outsz             // (optional for #outsz != 0)
    
  2. チェーン、#localsz > 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        sub    sp,sp,#(localsz+outsz)   // allocate remaining frame
        stp    x29,lr,[sp,#outsz]       // save <x29,lr> at bottom of local area
        add    x29,sp,#outsz            // setup x29 points to bottom of local area
    
  3. 非チェーンのリーフ関数 (lr 未保存)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]
        str    x23,[sp,#32]
        stp    d8,d9,[sp,#40]           // save FP regs (optional)
        stp    d10,d11,[sp,#56]
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    すべてのローカルに基づいて spアクセスされます。 <x29,lr> は前のフレームを指します。 フレーム サイズ <= 512 の場合、 sub sp, ... regs の保存領域がスタックの一番下に移動された場合に最適化できます。 欠点は、上記の他のレイアウトと一致しないことです。 また、保存された regs は、ペア regs とインデックス付きオフセットアドレス指定モードの前と後の範囲の一部を取ります。

  4. 非チェーンの非リーフ関数 (Int の保存領域に保存 lr )

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        stp    x23,lr,[sp,#32]          // save last Int reg and lr
        stp    d8,d9,[sp,#48]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#64]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    または、偶数値の保存 Int レジスタの場合、

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        str    lr,[sp,#32]              // save lr
        stp    d8,d9,[sp,#40]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#56]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    保存のみ x19 :

        sub    sp,sp,#16                // reg save area allocation*
        stp    x19,lr,[sp]              // save x19, lr
        sub    sp,sp,#(framesz-16)      // allocate the remaining local area
    

    * 事前インデックス付き reg-lr stp をアンワインド コードで表すことができるため、reg セーブ エリアの割り当ては分割stpされません。

    すべてのローカルに基づいて spアクセスされます。 <x29> は前のフレームを指します。

  5. チェーン、#framesz <= 512、#outsz = 0

        stp    x29,lr,[sp,#-framesz]!       // pre-indexed, save <x29,lr>
        mov    x29,sp                       // x29 points to bottom of stack
        stp    x19,x20,[sp,#(framesz-32)]   // save INT pair
        stp    d8,d9,[sp,#(framesz-16)]     // save FP pair
    

    上記の最初のプロローグの例と比較すると、この例には利点があります。すべてのレジスタ保存命令は、1 つのスタック割り当て命令の後でのみ実行できます。 つまり、命令レベルの並列処理を妨げる反依存 sp はありません。

  6. チェーン、フレーム サイズ > 512 (関数がない alloca場合は省略可能)

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        sub    sp,sp,#(framesz-80)          // allocate the remaining local area
    

    最適化のために、 x29 ローカル領域の任意の位置に配置して、"reg-pair" と pre-/post-indexed オフセット アドレッシング モードのカバレッジを向上させることができます。 フレーム ポインターの下のローカルには、次に基づいて spアクセスできます。

  7. チェーン、フレーム サイズ > 4K、alloca() 有りまたは無し

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        mov    x15,#(framesz/16)
        bl     __chkstk
        sub    sp,sp,x15,lsl#4              // allocate remaining frame
                                            // end of prolog
        ...
        sub    sp,sp,#alloca                // more alloca() in body
        ...
                                            // beginning of epilog
        mov    sp,x29                       // sp points to top of local area
        ldp    d10,d11,[sp,#64]
        ...
        ldp    x29,lr,[sp],#80              // post-indexed, reload <x29,lr>
    

ARM64 例外処理情報

.pdata レコード

レコードは .pdata 、PE バイナリ内のすべてのスタック操作関数を記述する固定長項目の順序付けられた配列です。 "スタック操作" という語句は重要です。ローカル ストレージを必要とせず、不揮発性レジスタを保存/復元する必要がなく、レコードを必要 .pdata としないリーフ関数。 領域を節約するために、これらのレコードを明示的に省略する必要があります。 これらの関数の 1 つからのアンワインドは、呼び出し元に移動するために直接 lr 戻りアドレスを取得できます。

ARM64 の各 .pdata レコードの長さは 8 バイトです。 各レコードの一般的な形式では、関数の 32 ビット RVA が最初の単語で始まり、次に可変長 .xdata ブロックへのポインターまたは正規関数アンワインド シーケンスを記述するパックされた単語を含む 2 番目の単語が配置されます。

.pdata record layout.

フィールドは次のとおりです。

  • Function Start RVA は、関数の開始の 32 ビット の RVA です。

  • フラグは、2 番目.pdataの単語の 30 ビットメイン再解釈する方法を示す 2 ビット フィールドです。 フラグが 0 の場合、残りのビットは例外情報 RVA (2 つの最下位ビットが暗黙的に 0) を形成します。 フラグが 0 以外の場合、残りのビットはパックされたアンワインド データ構造体を形成します。

  • "例外情報 RVA" は、.xdata セクションに格納されている可変長例外情報構造体のアドレスです。 このデータは、4 バイトでアラインされている必要があります。

  • パックされたアンワインド データは、関数からのアンワインドに必要な演算の圧縮記述です (標準の形式を想定)。 この場合、.xdata レコードは必要ありません。

.xdata レコード

パックされたアンワインド形式では関数のアンワインドの記述に十分でない場合、可変長の .xdata レコードを作成する必要があります。 このレコードのアドレスは、.pdata レコードの第 2 ワードに格納されています。 この形式 .xdata は、パックされた可変長の単語セットです。

.xdata record layout.

このデータは、次の 4 つのセクションに分かれています。

  1. 構造体の全体的なサイズを記述し、キー関数データを提供する 1 単語または 2 ワードのヘッダー。 2 番目のワードは、エピローグ カウントコード ワード フィールドの両方が 0 に設定されている場合にのみ存在します。 ヘッダーには、次のビット フィールドがあります。

    a. Function Length は 18 ビット フィールドです。 これは関数全体の長さを 4 で除算してバイト単位で示します。 関数が 1M を超える場合は、複数 .pdata のレコードを .xdata 使用して関数を記述する必要があります。 詳細については、「大きな関数」セクションを参照してください。

    b. Vers は 2 ビットフィールドです。 再メインのバージョンについて説明します.xdata。 現時点では、バージョン 0 のみが定義されているため、1 ~ 3 の値は使用できません。

    c. X は 1 ビット フィールドです。 これは、例外データが存在する (1) か存在しない (0) かを示します。

    d. E は 1 ビット フィールドです。 これは、1 つのエピローグを記述する情報が、後でより多くのスコープワード (0) を必要とするのではなく、ヘッダー (1) にパックされていることを示します。

    e. Epilog Count は、E ビットの状態に応じて 2 つの意味を持つ 5 ビット フィールドです。

    1. E が 0 の場合は、セクション 2 で説明されているエピローグ スコープの合計数を指定します。 32 個以上のスコープが関数に存在する場合、Code Words フィールドが 0 に設定され、拡張ワードが必要であることを示す必要があります。

    2. E が 1 の場合、このフィールドは、エピローグのみを記述する最初のアンワインド コードのインデックスを指定します。

    f. Code Words は、すべてのアンワインド コードをセクション 3 に含めるために必要な 32 ビット ワードの数を指定する 5 ビット フィールドです。 31 を超える単語 (つまり、124 個のアンワインド コード) が必要な場合は、拡張単語が必要であることを示すには、このフィールドを 0 にする必要があります。

    g. Extended Epilog Count および Extended Code Words は、それぞれ 16 ビットおよび 8 ビットのフィールドです。 これらは、エピローグの数が通常よりも多い場合や、アンワインド コード ワードの数が通常よりも多い場合に、エンコーディング用の追加の領域を提供します。 これらのフィールドを含む拡張ワードは、1 番目のヘッダー ワードの Epilog CountCode Words フィールドの両方が 0 の場合にのみ存在します。

  2. エピローグの数が 0 でない場合は、エピローグ スコープに関する情報の一覧が、1 単語にパックされ、ヘッダーとオプションの拡張ヘッダーの後に表示されます。 これらは、開始オフセットが増加する順に格納されます。 各スコープには、以下のビットが含まれています。

    a. Epilog Start Offset は、関数の開始を基準にしたエピローグの (バイト単位の) オフセットを 4 で除算した値を示す 18 ビット フィールドです。

    b. Res は、将来の拡張用に予約された 4 ビット フィールドです。 この値は 0 である必要があります。

    c. Epilog Start Index は 10 ビット フィールドです (Extended Code Words よりも 2 ビット多い)。 これは、このエピローグを記述する最初のアンワインド コードのバイト インデックスを示します。

  3. エピローグ スコープのリストの後には、アンワインド コードを含むバイトの配列が続きます (詳細については後のセクションで説明します)。 この配列は、最も近いフルワード境界の末尾に埋め込まれます。 アンワインド コードは、この配列に書き込まれます。 これらは関数の本体に最も近いものから開始し、関数の端に向かって移動します。 各アンワインド コードのバイトはビッグ エンディアン順に格納されるため、最も重要なバイトが最初にフェッチされ、操作と残りのコードの長さが識別されます。

  4. 最後に、ヘッダーの X ビットが 1 に設定されている場合、アンワインド コード バイトの後に例外ハンドラー情報が続きます。 これは、例外ハンドラー自体のアドレスを提供する単一の例外ハンドラー RVA で構成されます。 その直後に、例外ハンドラーに必要な可変長のデータが続きます。

.xdataこのレコードは、最初の 8 バイトをフェッチし、それを使用してレコードのフル サイズから、次の可変長例外データの長さを引いた値を計算できるように設計されています。 次のコード スニペットはレコードのサイズを計算します。

ULONG ComputeXdataSize(PULONG Xdata)
{
    ULONG Size;
    ULONG EpilogScopes;
    ULONG UnwindWords;

    if ((Xdata[0] >> 22) != 0) {
        Size = 4;
        EpilogScopes = (Xdata[0] >> 22) & 0x1f;
        UnwindWords = (Xdata[0] >> 27) & 0x1f;
    } else {
        Size = 8;
        EpilogScopes = Xdata[1] & 0xffff;
        UnwindWords = (Xdata[1] >> 16) & 0xff;
    }

    if (!(Xdata[0] & (1 << 21))) {
        Size += 4 * EpilogScopes;
    }

    Size += 4 * UnwindWords;

    if (Xdata[0] & (1 << 20)) {
        Size += 4;  // Exception handler RVA
    }

    return Size;
}

プロローグおよび各エピローグにはアンワインド コードの独自のインデックスがありますが、表はこれらの間で共有されます。 これらすべてが同じコードを共有することは可能です (全く珍しいことではありません)。 (例については、次の例 2 を参照してください。 [例] セクション)。コンパイラ ライターは、この場合に特に最適化する必要があります。 これは、指定できる最大のインデックスが 255 であり、特定の関数のアンワインド コードの合計数が制限されるためです。

アンワインド コード

アンワインド コードの配列は、プロローグの効果を元に戻す方法を正確に記述するシーケンスのプールです。 これらは、操作を元に戻す必要があるのと同じ順序で格納されます。 アンワインド コードは、バイト列としてエンコードされた小さな命令セットと見なすことができます。 実行が完了すると、呼び出し元の関数へのリターン アドレスがレジスタに格納されます lr 。 また、すべての非揮発性レジスタは、関数が呼び出された時点の値が復元されます。

例外が関数本体のみで発生し、プロローグまたはエピローグでは絶対に発生しないことが保証されている場合、必要なシーケンスは 1 つのみです。 ただし、Windows アンワインド モデルでは、部分的に実行されたプロローグまたはエピローグからコードをアンワインドできる必要があります。 この要件を満たすため、アンワインド コードはプロローグおよびエピローグ内の各関連オペコードに 1:1 で明確にマッピングするように慎重に設計されています。 この設計は、次のような結果をもたらします。

  • アンワインド コードの数のカウントにより、プロローグおよびエピローグの長さの計算が可能です。

  • エピローグ スコープの開始後の命令の数をカウントすることにより、同じ数のアンワインド コードをスキップできます。 残りのシーケンスを実行して、エピローグによって部分的に実行されたアンワインドを完了できます。

  • プロローグの終了前の命令の数をカウントすることにより、同じ数のアンワインド コードをスキップできます。 シーケンスの残りの部分を実行して、実行が完了したプロローグの部分のみを元に戻すことができます。

アンワインド コードは、次の表に従ってエンコードされます。 すべてのアンワインド コードは、巨大なスタック (alloc_l) を割り当てるコードを除き、1 バイトまたは 2 バイトです。 合計で 22 個のアンワインド コードがあります。 各アンワインド コードは、部分的に実行されたプロローグとエピローグのアンワインドを可能にするために、プロローグ/エピローグ内の 1 つの命令に厳密にマップされます。

アンワインド コード ビットと解釈
alloc_s 000xxxxx: サイズ < 512 (2^5 * 16) の小さいスタックを割り当てます。
save_r19r20_x 001zzzzz: save <x19,x20> pair at [sp-#Z*8]!, pre-indexed offset >= -248
save_fplr 01zzzzzz: <x29,lr> のペアを [sp+#Z*8] に保存、オフセット <= 504。
save_fplr_x 10zzzzzz: ペアを 、[sp-(#Z+1)*8]!インデックス付きオフセットの前に>保存 <x29,lr> = -512
alloc_m 11000xxx'xxxxxxxx: サイズ < 32K (2^11 * 16) の大きなスタックを割り当てます。
save_regp 110010xx'xxzzzzzz: save x(19+#X) pair at [sp+#Z*8], offset <= 504
save_regp_x 110011xx'xxzzzzzz: save pair x(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
save_reg 110100xx'xxzzzzzz: reg x(19+#X) を 、offset <= 504 に[sp+#Z*8]保存します
save_reg_x 1101010x'xxxzzzzz: reg x(19+#X) を 、事前インデックス付きオフセット >= -256 に[sp-(#Z+1)*8]!保存
save_lrpair 1101011x'xxzzzzzz: ペア <x(19+2*#X),lr>[sp+#Z*8] に保存、オフセット <= 504
save_fregp 1101100x'xxzzzzzz: save pair d(8+#X) at [sp+#Z*8], offset <= 504
save_fregp_x 1101101x'xxzzzzzz: save pair d(8+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
save_freg 1101110x'xxzzzzzz: reg d(8+#X) をオフセット <= 504 で[sp+#Z*8]保存
save_freg_x 11011110'xxxzzzzz: reg を d(8+#X)[sp-(#Z+1)*8]!事前インデックス付きオフセット >= -256 に保存します
alloc_l 111000000'xxxxxxxx'xxxxxxxx'xxxxxxxx: サイズ < 256M の大きなスタックを割り当てます (2^24 * 16)
set_fp 11100001:x29mov x29,sp
add_fp 11100010'xxxxxxxx: で設定 x29 する add x29,sp,#x*8
nop 11100011: アンワインド演算は必要ありません。
end 11100100: アンワインド コードの終わりです。 エピローグで意味します ret
end_c 11100101: 現在のチェーン スコープのアンワインド コードの終わりです。
save_next 11100110: 次の非揮発性 Int または FP レジスタ ペアを保存します。
11100111: 予約済み
11101xxx: asm ルーチンでのみ生成される以下のカスタム スタック ケース用に予約
11101000: カスタム スタック MSFT_OP_TRAP_FRAME
11101001: カスタム スタック MSFT_OP_MACHINE_FRAME
11101010: カスタム スタック MSFT_OP_CONTEXT
11101011: カスタム スタック MSFT_OP_EC_CONTEXT
11101100: カスタム スタック MSFT_OP_CLEAR_UNWOUND_TO_CALL
11101101: 予約済み
11101110: 予約済み
11101111: 予約済み
11110xxx: 予約済み
11111000'yyyyyyy: 予約済み
11111001'yyyyyyyy'yyyyyyyy: 予約済み
11111010'yyyyyyy'yyyyyyyy'yyyyyyyy: reserved
11111011'yyyyyyy'yyyyyyy'yyyyyyyy'yyyyyyyy: reserved
pac_sign_lr 11111100: <11111100:pacibsp
11111101: 予約済み
11111110: 予約済み
11111111: 予約済み

複数のバイトをカバーする大きな値を持つ命令では、最上位ビットが最初に格納されます。 この設計により、コードの最初のバイトだけを検索することで、アンワインド コードの合計サイズをバイト単位で見つけられるようになります。 各アンワインド コードは、プロローグまたはエピローグ内の命令に厳密にマップされているため、プロローグまたはエピローグのサイズを計算できます。 シーケンスの先頭から末尾まで移動し、ルックアップ テーブルまたは同様のデバイスを使用して、対応するオペコードの長さを決定します。

事後インデックスされたオフセット アドレス指定はプロローグ内では許可されていません。 すべてのオフセット範囲 (#Z) は、すべての保存領域に対して 248 で十分である (10 Int レジスタ + 8 FP レジスタ + 8 入力レジスタ) を除くsave_r19r20_xアドレッシングのエンコードstrstp/と一致します。

save_next は、Int または FP 揮発性レジスタ ペアの保存に従う必要があります(save_regpsave_regp_xsave_fregpsave_fregp_xsave_r19r20_x、または別の save_next)。 これは次のレジスタ ペアを "拡大" 順で次の 16 バイトスロットに保存します。 save_next は、最後の Int レジスタ ペアを表す save-next に続く場合、最初の FP レジスタ ペアを参照します。

通常のリターン命令とジャンプ命令のサイズは同じであるため、末尾呼び出しシナリオでは分離された end アンワインド コードは必要ありません。

end_c は、連続していない関数フラグメントを最適化のために処理するように設計されています。 end_c現在のスコープ内のアンワインド コードの末尾を示す an は、次に、実際endのコードで終わる別の一連のアンワインド コードを続ける必要があります。 アンワインド コード end_cend 親リージョンのプロローグ操作 ("ファントム" プロローグ) を表します。 詳細と例については、以下のセクションで説明します。

パックされたアンワインド データ

関数のプロローグおよびエピローグが下に示す標準形式に従っている場合、パックされたアンワインド データを使用できます。 レコードの必要性 .xdata を完全になくし、アンワインド データを提供するコストを大幅に削減します。 正規のプロローグとエピローグは、単純な関数の一般的な要件を満たすように設計されています。例外ハンドラーを必要とせず、標準の順序でセットアップと破棄の操作を行います。

パックされたアンワインド データを .pdata 含むレコードの形式は次のようになります。

.pdata record with packed unwind data.

フィールドは次のとおりです。

  • Function Start RVA は、関数の開始の 32 ビット の RVA です。
  • フラグ は、上で説明した 2 ビット フィールドで、次の意味を持ちます。
    • 00 = パックされたアンワインド データは使用されません。再メインビットがレコードを.xdata指す
    • 01 = スコープの先頭と末尾で 1 つのプロローグとエピローグで使用される、パックされたアンワインド データ
    • 10 = プロローグとエピローグのないコードに使用される、パックされたアンワインド データ。 分離された関数セグメントを記述する場合に便利です
    • 11 = 予約済み。
  • Function Length は、関数全体の長さを 4 で除算してバイト単位で示す 11 ビット フィールドです。 関数が 8k を超える場合は、代わりに完全 .xdata なレコードを使用する必要があります。
  • Frame Size は、この関数に割り当てられたスタックのバイト数を 16 で除算した値を示す 9 ビット フィールドです。 (8k から 16) バイトを超えるスタックを割り当てる関数は、完全 .xdata なレコードを使用する必要があります。 これには、ローカル変数領域、発信パラメーター領域、呼び出し先が保存した Int および FP 領域、およびホーム パラメーター領域が含まれます。 動的割り当て領域は除外されます。
  • CR は、フレーム チェーンとリターン リンクを設定するための追加命令が関数に含まれているかどうかを示す 2 ビット フラグです。
    • 00 = 非チェーン関数、 <x29,lr> ペアはスタックに保存されません
    • 01 = 非チェーン関数、<lr> はスタックに保存されます
    • 10 = 符号付きリターン アドレスを pacibsp 持つチェーン関数
    • 11 = チェーン関数、ストア/ロード ペア命令がプロローグ/エピローグ <x29,lr> で使用されます
  • H は、関数が、整数パラメーター レジスタ (x0-x7) を関数の先頭に格納することによって、それらを元に戻すかどうかを示す 1 ビット フラグです。 (0 = ホーム レジスタではない、1 = ホーム レジスタ)。
  • RegI は、標準スタックの場所に保存された非揮発性 INT レジスタ (x19-x28) の数を示す 4 ビット フィールドです。
  • RegF は、標準スタックの場所に保存された非揮発性 FP レジスタ (d8-d15) の数を示す 3 ビット フィールドです。 (RegF=0: FP レジスタは保存されません。RegF>0: RegF+1 FP レジスタが保存されます)。 パックされたアンワインド データは、1 つの FP レジスタだけを保存する関数には使用できません。

上記のセクションのカテゴリ 1、2 (送信パラメーター領域なし)、3 および 4 に分類される標準プロローグは、パックされたアンワインド形式で表すことができます。 標準関数のエピローグは同様の形式に従いますが、H が影響を及ぼさないこと、set_fp 命令が省略されること、および手順の順序と各手順の命令が、エピローグ内で反転されている点が異なります。 パック .xdata のアルゴリズムは、次の表で詳しく説明するこれらの手順に従います。

手順 0: 各領域のサイズを事前に計算します。

手順 1: 返送先住所に署名します。

手順 2: Int 呼び出し先で保存されたレジスタを保存します。

手順 3: この手順は、初期セクションのタイプ 4 に固有です。 lr は Int 領域の末尾に保存されます。

手順 4: FP 呼び出し先が保存したレジスタを保存します。

手順 5: ホーム パラメーター領域に入力引数を保存します。

手順 6: ローカル領域メインペア、発信パラメーター領域など、<x29,lr>再メインスタックを割り当てます。 6a は正規型 1 に対応します。 6b と 6c は正規型 2 用です。 6d と 6e は、タイプ 3 とタイプ 4 の両方を対象とします。

手順番号 フラグの値 命令の数 オペコード アンワインド コード
0 #intsz = RegI * 8;
if (CR==01) #intsz += 8; // lr
#fpsz = RegF * 8;
if(RegF) #fpsz += 8;
#savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf)
#locsz = #famsz - #savsz
1 CR == 10 1 pacibsp pac_sign_lr
2 0 <RegI<= 10 RegI / 2 +
RegI % 2
stp x19,x20,[sp,#savsz]!
stp x21,x22,[sp,#16]
...
save_regp_x
save_regp
...
3 CR == 01* 1 str lr,[sp,#(intsz-8)]* save_reg
4 0 <RegF<=7 (RegF + 1) / 2 +
(RegF + 1) % 2)
stp d8,d9,[sp,#intsz]**
stp d10,d11,[sp,#(intsz+16)]
...
str d(8+RegF),[sp,#(intsz+fpsz-8)]
save_fregp
...
save_freg
5 H == 1 4 stp x0,x1,[sp,#(intsz+fpsz)]
stp x2,x3,[sp,#(intsz+fpsz+16)]
stp x4,x5,[sp,#(intsz+fpsz+32)]
stp x6,x7,[sp,#(intsz+fpsz+48)]
nop
nop
nop
nop
6a (CR == 10 || CR == 11) >
#locsz<= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6b (CR == 10 || CR == 11) >
512 <#locsz<= 4080
3 sub sp,sp,#locsz
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
save_fplr
set_fp
6c (CR == 10 || CR == 11) >
#locsz> 4080
4 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
alloc_s/alloc_m
save_fplr
set_fp
6d (CR == 00 || CR == 01) &
#locsz<= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6e (CR == 00 || CR == 01) &
#locsz> 4080
2 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
alloc_m
alloc_s/alloc_m

* CR == 01 で RegI が奇数の場合、ステップ 2 と手順 1 の最後save_repの値が 1 にsave_regpマージされます。

** RegI == CR == 0、RegF != 0 の場合、浮動小数点の最初stpの関数は事前宣言を行います。

*** mov x29,sp に対応する命令はエピローグに存在しません。 関数からx29復元spする必要がある場合、パックされたアンワインド データは使用できません。

部分的なプロローグおよびエピローグのアンワインド

最も一般的なアンワインドの状況では、プロローグとすべてのエピローグから離れて、関数の本体で例外または呼び出しが発生します。 このような状況では、アンワインドは簡単です。アンワインダーはアンワインド配列内のコードを実行するだけです。 インデックス 0 から始まり、オペコードが検出されるまで end 続行されます。

プロローグまたはエピローグの実行中に例外または割り込みが発生した場合、適切にアンワインドするのはより困難です。 このような状況では、スタック フレームは部分的にのみ構築されます。 問題は、何が行われたかを正確に判断し、正しく元に戻すことです。

たとえば、次のプロローグとエピローグ シーケンスを考えてみます。

0000:    stp    x29,lr,[sp,#-256]!          // save_fplr_x  256 (pre-indexed store)
0004:    stp    d8,d9,[sp,#224]             // save_fregp 0, 224
0008:    stp    x19,x20,[sp,#240]           // save_regp 0, 240
000c:    mov    x29,sp                      // set_fp
         ...
0100:    mov    sp,x29                      // set_fp
0104:    ldp    x19,x20,[sp,#240]           // save_regp 0, 240
0108:    ldp    d8,d9,[sp,224]              // save_fregp 0, 224
010c:    ldp    x29,lr,[sp],#256            // save_fplr_x  256 (post-indexed load)
0110:    ret    lr                          // end

各オペコードの横には、この演算を記述する適切なアンワインド コードがあります。 プロローグの一連のアンワインド コードは、エピローグのアンワインド コードの正確なミラー イメージになっていることが確認できます (エピローグの最後の命令はカウントされていません)。 これは一般的な状況です。プロローグのアンワインド コードがプロローグの実行順序とは逆の順序で格納されていると常に想定している理由です。

したがって、プロローグとエピローグの両方で、共通のアンワインド コードのセットが残されています。

set_fpsave_regp 0,240, save_fregp,0,224, save_fplr_x_256end

エピローグは通常の順序であるため、単純です。 エピローグ内のオフセット 0 (関数内のオフセット 0x100から始まる) から開始すると、クリーンのアップがまだ行われていないため、完全なアンワインド シーケンスが実行されます。 (エピローグのオフセット 2 で) 命令が 1 つある場合、最初のアンワインド コードをスキップして正常にアンワインドできます。 この状況を一般化し、オペコードとアンワインド コードの間に 1:1 のマッピングがあることを想定できます。 次に、エピローグで命令 n からアンワインドを開始するには、最初の n 個のアンワインド コードをスキップし、そこから実行を開始する必要があります。

これにより、(逆になることを除き) 同じようなロジックがプロローグに対して機能することがわかります。 プロローグ内のオフセット 0 からアンワインドを開始する場合は、何も実行しません。 1 つの命令があるオフセット 2 からアンワインドする場合は、末尾から 1 つのアンワインド コードのアンワインド シーケンスの実行を開始する必要があります。 (コードは逆の順序で格納されます。ここでも一般化できます。プロローグの命令 n からアンワインドを開始する場合は、コードの一覧の末尾から n 個のアンワインド コードの実行を開始する必要があります。

プロローグ コードとエピローグ コードが常に正確に一致するとは限らないため、アンワインド配列には複数のコード シーケンスを含める必要がある場合があります。 コードの処理を開始する位置のオフセットを特定するには、次のロジックを使用します。

  1. 関数の本体内からアンワインドする場合は、インデックス 0 でアンワインド コードの実行を開始し、オペコードに end 達するまで続行します。

  2. エピローグ内からのアンワインドの場合、エピローグ スコープにより開始ポイントとして提供されるエピローグ固有の開始インデックスを使用します。 エピローグの開始から問題の PC までのバイト数を計算します。 次に、アンワインド コードを前方に進め、既に実行済みの命令がすべて含まれるまでアンワインド コードをスキップします。 その後、その時点から実行します。

  3. プロローグ内からアンワインドする場合は、開始点としてインデックス 0 を使用します。 シーケンスからプロローグ コードの長さを計算し、プロローグの末尾から問題の PC までのバイト数を計算します。 次に、アンワインド コードを前方に進め、まだ実行されていない命令がすべて含まれるまでアンワインド コードをスキップします。 その後、その時点から実行します。

これらのルールは、プロローグのアンワインド コードが常に配列の最初に存在する必要があることを意味します。 また、これらは本体内からのアンワインドという一般的な場合のアンワインドに使用されるコードでもあります。 すべてのエピローグ固有のコード シーケンスは、直後に続く必要があります。

関数フラグメント

コードの最適化やその他の理由から、関数を分離されたフラグメント (リージョンとも呼ばれます) に分割することをお勧めします。 分割する場合、結果として得られる各関数フラグメントには、独自の個別 .pdata の (場合によっては) レコードが .xdata必要です。

独自のプロローグを持つ分離された各セカンダリ フラグメントでは、そのプロローグ内でスタック調整が行われないことが想定されます。 セカンダリ リージョンに必要なすべてのスタック領域は、その親リージョン (またはホスト リージョンとも呼ばれます) によって事前に割り当てられている必要があります。 この事前割り当てにより、スタック ポインター操作が関数の元のプロローグに厳密に保持されます。

関数フラグメントの一般的なケースは"コード分離"であり、コンパイラはコードの領域をホスト関数から移動する可能性があります。 コードの分離の結果として生じる可能性がある 3 つの異常なケースがあります。

  • (リージョン 1: 開始)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (リージョン 1: 終了)

  • (リージョン 3: 開始)

        ...
    
  • (リージョン 3: 終了)

  • (リージョン 2: 開始)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (リージョン 2: 終了)

  1. プロローグのみ (リージョン 1: すべてのエピローグは分離されたリージョンにあります):

    プロローグだけを記述する必要があります。 このプロローグをコンパクト .pdata 形式で表すことはできません。 完全 .xdata なケースでは、Epilog Count = 0 を設定することで表すことができます。 上の例のリージョン 1 を参照してください。

    アンワインド コード: set_fpsave_regp 0,240save_fplr_x_256end

  2. エピローグのみ (リージョン 2: プロローグはホスト リージョンにあります)

    コントロールがこの領域にジャンプする時点までに、すべてのプロローグ コードが実行されていると想定されます。 部分アンワインドが、通常の関数と同じようにエピローグで発生する可能性があります。 この種類の領域はコンパクトで .pdata表すことはできません。 完全.xdataなレコードでは、アンワインド コード ペアで角かっこで囲まれた "ファントム" プロローグでend_cendエンコードできます。 先頭の end_c は、プロローグのサイズが 0 であることを示します。 単一のエピローグのエピローグ開始インデックスは、set_fp を指しています。

    リージョン 2 のアンワインド コード: end_cset_fpsave_regp 0,240save_fplr_x_256end

  3. プロローグもエピローグもない (リージョン 3: プロローグおよびすべてのエピローグが他のフラグメントに含まれている):

    コンパクト .pdata 形式は、フラグ = 10 を設定して適用できます。 完全な .xdata レコードの場合、エピローグ数 = 1。 アンワインド コードは上記のリージョン 2 のコードと同じですが、Epilog Start Index も end_c を指しています。 このコードのリージョンでは部分アンワインドは発生しません。

関数フラグメントのもう 1 つの複雑なケースとして、"シュリンク ラッピング" があります。コンパイラによって、関数エントリのプロローグに含まれなくなるまで、一部の呼び出し先保存レジスタの保存が遅延される場合があります。

  • (リージョン 1: 開始)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (リージョン 2: 開始)

        stp     x21,x22,[sp,#224]       // save_regp 2, 224
        ...
        ldp     x21,x22,[sp,#224]       // save_regp 2, 224
    
  • (リージョン 2: 終了)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (リージョン 1: 終了)

リージョン 1 のプロローグには、スタック領域が事前に割り当てられています。 リージョン 2 は、ホスト関数の外に移されても、同じアンワインド コードを持つことがわかります。

領域 1: set_fp, , save_regp 0,240, save_fplr_x_256. end エピローグ開始インデックスは通常どおりを指します set_fp

リージョン 2: save_regp 2, 224end_cset_fpsave_regp 0,240save_fplr_x_256end。 Epilog Start Index は、最初のアンワインド コード save_regp 2, 224 を指します。

大きな関数

フラグメントは、ヘッダーのビット フィールドによって課される 1M の制限を超える関数を .xdata 記述するために使用できます。 このような異常に大きな関数を記述するには、1M 未満のフラグメントに分割する必要があります。 各フラグメントは、エピローグが複数の部分に分割されないように調整する必要があります。

関数の最初のフラグメントにのみプロローグが含まれます。その他のすべてのフラグメントは、プロローグが含まれていないとマークされます。 存在するエピローグの数に応じて、各フラグメントに 0 個以上のエピローグが含まれます。 フラグメント内の各エピローグ スコープでは、関数の開始からではなく、フラグメントの開始からの相対値で開始オフセットが指定されることに注意してください。

フラグメントにプロローグがなく、エピローグがない場合でも、関数の本体内からアンワインドする方法を記述するために、独自 .pdata の (および場合 .xdataによっては) レコードが必要です。

例 1: フレームチェーン型、コンパクトフォーム

|Foo|     PROC
|$LN19|
    str     x19,[sp,#-0x10]!        // save_reg_x
    sub     sp,sp,#0x810            // alloc_m
    stp     fp,lr,[sp]              // save_fplr
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...

|$pdata$Foo|
    DCD     imagerel     |$LN19|
    DCD     0x416101ed
    ;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]

例 2: ミラー Prolog と Epilog を使用したフレームチェーン、フル フォーム

|Bar|     PROC
|$LN19|
    stp     x19,x20,[sp,#-0x10]!    // save_regp_x
    stp     fp,lr,[sp,#-0x90]!      // save_fplr_x
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...
                                    // begin of epilog, a mirror sequence of Prolog
    mov     sp,fp
    ldp     fp,lr,[sp],#0x90
    ldp     x19,x20,[sp],#0x10
    ret     lr

|$pdata$Bar|
    DCD     imagerel     |$LN19|
    DCD     imagerel     |$unwind$cse2|
|$unwind$Bar|
    DCD     0x1040003d
    DCD     0x1000038
    DCD     0xe42291e1
    DCD     0xe42291e1
    ;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
    ;Epilog Start Index[0], Epilog Start Offset[56]
    ;set_fp
    ;save_fplr_x
    ;save_r19r20_x
    ;end

Epilog Start Index [0] は、プロローグ アンワインド コードの同じシーケンスを指します。

例 3: 可変個の非チェーン関数

|Delegate| PROC
|$LN4|
    sub     sp,sp,#0x50
    stp     x19,lr,[sp]
    stp     x0,x1,[sp,#0x10]        // save incoming register to home area
    stp     x2,x3,[sp,#0x20]        // ...
    stp     x4,x5,[sp,#0x30]
    stp     x6,x7,[sp,#0x40]        // end of prolog
    ...
    ldp     x19,lr,[sp]             // beginning of epilog
    add     sp,sp,#0x50
    ret     lr

    AREA    |.pdata|, PDATA
|$pdata$Delegate|
    DCD     imagerel |$LN4|
    DCD     imagerel |$unwind$Delegate|

    AREA    |.xdata|, DATA
|$unwind$Delegate|
    DCD     0x18400012
    DCD     0x200000f
    DCD     0xe3e3e3e3
    DCD     0xe40500d6
    DCD     0xe40500d6
    ;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
    ;Epilog Start Index[4], Epilog Start Offset[15]
    ;nop        // nop for saving in home area
    ;nop        // ditto
    ;nop        // ditto
    ;nop        // ditto
    ;save_lrpair
    ;alloc_s
    ;end

Epilog Start Index [4] は、プロローグ アンワインド コードの中間を指します (アンワインド配列を部分的に再利用します)。

関連項目

ARM64 ABI 規則の概要
ARM 例外処理