次の方法で共有


ARM64 例外処理

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

目的と動機

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

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

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

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

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

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

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

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

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

前提条件

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

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

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

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

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

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

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

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

関数のスタック フレームのレイアウトを示す図。

フレームチェーン関数の場合、最適化の考慮事項に応じて、 fplr のペアをローカル変数領域内の任意の位置に保存できます。 目標は、フレーム ポインター (x29) またはスタック ポインター (sp) に基づいて、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 の場合、regs の保存領域をスタックの下部に移動すると、 sub sp, ... を最適化できます。 欠点は、上記の他のレイアウトと一致しないことです。 また、保存された 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 save area allocation't folded into the stp because a pre-indexed reg-lr stp can't represented with the unwind codes.

    すべてのローカルには、 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 レコードは必要ありません。 領域を節約するために、これらのレコードを明示的に省略する必要があります。 これらの関数のいずれかからのアンワインドは、 lr から直接戻りアドレスを取得して、呼び出し元に移動できます。

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

.pdata レコードのレイアウト。

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

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

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

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

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

.xdata

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

.xdata レコードのレイアウト。

このデータは、次の 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: [sp-#Z*8]!<x19,x20>ペアを保存し、事前インデックス付きオフセット >= -248
save_fplr 01zzzzzz: <x29,lr> のペアを [sp+#Z*8] に保存、オフセット <= 504。
save_fplr_x 10zzzzzz: <x29,lr> ペアを [sp-(#Z+1)*8]!、事前インデックス付きオフセット >= -512 に保存します
alloc_m 11000xxx'xxxxxxxx: サイズが 32K (2^11 * 16) < 大きなスタックを割り当てます。
save_regp 110010xx'xxzzzzzz: [sp+#Z*8]、オフセット <= 504 でx(19+#X)ペアを保存
save_regp_x 110011xx'xxzzzzzz: [sp-(#Z+1)*8]!、事前インデックス付きオフセット >= -512 でペア x(19+#X)を保存する
save_reg 110100xx'xxzzzzzz: reg x(19+#X)[sp+#Z*8]、オフセット <= 504 に保存します
save_reg_x 1101010x'xxxzzzzz: reg x(19+#X)[sp-(#Z+1)*8]!、事前インデックス付きオフセット >= -256 に保存します
save_lrpair 1101011x'xxzzzzzz: ペア <x(19+2*#X),lr>[sp+#Z*8] に保存、オフセット <= 504
save_fregp 1101100x'xxzzzzzz: [sp+#Z*8]、オフセット <= 504 でペア d(8+#X)を保存
save_fregp_x 1101101x'xxzzzzzz: [sp-(#Z+1)*8]!、事前インデックス付きオフセット >= -512 でペア d(8+#X)を保存
save_freg 1101110x'xxzzzzzz: reg d(8+#X)[sp+#Z*8] で保存し、オフセット <= 504
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 x29 : mov 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: <でリターン アドレスに署名するpacibsp
11111101: 予約済み
11111110: 予約済み
11111111: 予約済み

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

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

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 の後に、実際の endで終わる別の一連のアンワインド コードが続く必要があります。 end_cendの間のアンワインド コードは、親リージョン ("ファントム" プロローグ) でのプロローグ操作を表します。 詳細と例については、以下のセクションで説明します。

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

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

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

パックされたアンワインド データを含む .pdata レコード。

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

  • 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 が奇数の場合、手順 3 と手順 2 の最後の save_reg は 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,240save_fregp,0,224save_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 までのバイト数を計算します。 次に、アンワインド コードを前方に進め、まだ実行されていない命令がすべて含まれるまでアンワインド コードをスキップします。 その後、その時点から実行します。

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

関数フラグメント

コードの最適化などの理由から、関数を分離されたフラグメント ( regions とも呼ばれます) に分割することをお勧めします。 分割すると、結果として得られる各関数フラグメントには、独自の個別の .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_fpsave_regp 0,240save_fplr_x_256end。 エピローグ開始インデックスは、通常どおり set_fp を指します。

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

大きな関数

フラグメントは、 .xdata ヘッダーのビット フィールドによって課される 1M の制限を超える関数を記述するために使用できます。 このような異常に大きな関数を記述するには、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: フレームチェーン、フルフォーム、ミラープロローグ & エピローグ

|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 例外処理