ARM64 例外処理
ARM64 版 Windows は、ハードウェアで生成される非同期例外とソフトウェアで生成される同期例外に対して、同じ構造化例外処理メカニズムを使用します。 言語固有の例外ハンドラーは、言語ヘルパー関数を使用することで、Windows 構造化例外処理に付加して構築します。 このドキュメントでは、ARM64 上の Windows での例外処理について説明します。 Microsoft ARM アセンブラーと MSVC コンパイラによって生成されるコードで使用される言語ヘルパーを示します。
目的と動機
例外アンワインド データ規則およびこの記述は、次のことを目的としています。
どのような場合でも、コード プローブを使用しないアンワインドができるように十分な記述を提供します。
コードを分析するには、コードをページインする必要があります。 これにより、便利な状況 (トレース、サンプリング、デバッグ) でのアンワインドが防止されます。
コードの分析は複雑です。コンパイラは、アンワインダーがデコードできる命令だけを生成するように注意する必要があります。
アンワインド コードを使用してアンワインドを完全に記述できない場合は、場合によっては命令デコードにフォールバックする必要があります。 命令デコードにより全体的な複雑さが増し、理想的には回避する必要があります。
プロローグ中およびエピローグ中のアンワインドをサポートします。
- アンワインドは Windows で例外処理以外の目的でも使用されます。 プロローグまたはエピローグのコード シーケンスの途中であっても、コードを正確にアンワインドできることが重要です。
使用する容量を最小限にします。
バイナリ サイズが大幅に増加するようなアンワインド コードの集約はしないでください。
アンワインド コードはメモリ内でロックされる可能性が高いため、フットプリントを小さくすると、読み込まれる各バイナリのオーバーヘッドが最小限に抑えられます。
前提条件
例外処理の記述では、次のことが前提となっています。
通常、プロローグとエピローグは、相互にミラー化されます。 この共通の特徴を利用することで、アンワインドを記述するために必要なメタデータのサイズを大幅に削減できます。 関数本体内で、プロローグの演算が元に戻されるか、先に進んでエピローグの演算が実行されるかは関係ありません。 両方の場合で同じ結果となります。
関数は全体として比較的小さい傾向があります。 領域の最適化のいくつかは、データの最も効率的なパッキングを実現するために、この事実に依存しています。
エピローグに条件付きコードはありません。
専用フレーム ポインター レジスタ:
sp
がプロローグ内の別のレジスタ (x29
) に保存されている場合、そのレジスタは関数全体で変更されません。 これは、元のsp
がいつでも回復されることを意味します。sp
が別のレジスタに保存されない限り、スタック ポインターのすべての操作はプロローグとエピローグ内で厳密に行われます。スタック フレームのレイアウトは、次のセクションで説明するように構成されています。
ARM64 のスタック フレームのレイアウト
フレームチェーン関数の場合、最適化の考慮事項に応じて、 fp
と lr
のペアをローカル変数領域内の任意の位置に保存できます。 目標は、フレーム ポインター (x29
) またはスタック ポインター (sp
) に基づいて、1 つの命令で到達できるローカルの数を最大化することです。 ただし、 alloca
関数の場合は、チェーンする必要があり、 x29
はスタックの下部を指す必要があります。 レジスタ ペア アドレス指定モードのカバレッジを向上させるために、不揮発性レジスタの保存領域がローカル領域スタックの最上位に配置されます。 ここでは、最も効率的なプロローグ シーケンスの例をいくつか紹介します。 わかりやすくするため、およびキャッシュの局所性を向上させるために、すべての標準プロローグでの、呼び出し先保存レジスタを格納する順序は "拡大" 順です。 #framesz
以下はスタック全体のサイズを表します ( alloca
領域を除く)。 #localsz
と #outsz
は、それぞれローカル領域のサイズ (<x29, lr>
ペアの保存領域を含む)、および出力パラメーターのサイズを表します。
チェーン、#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)
チェーン、#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
非チェーンのリーフ関数 (
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 とインデックス付きオフセットアドレス指定モードの前と後の範囲の一部を取ります。非チェーンの非リーフ関数 (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-lrstp
can't represented with the unwind codes.すべてのローカルには、
sp
に基づいてアクセスされます。<x29>
は前のフレームを指します。チェーン、#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
に対する反依存はありません。チェーンされたフレーム サイズ > 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
に基づいてアクセスできます。チェーン、フレーム サイズ > 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 番目の単語、または正規関数アンワインド シーケンスを記述するパックされた単語が配置されます。
フィールドは次のとおりです。
Function Start RVA は、関数の開始の 32 ビット の RVA です。
Flag は、2 番目の
.pdata
ワードの残りの 30 ビットを解釈する方法を示す 2 ビット フィールドです。 フラグが 0 の場合、残りのビットは例外情報 RVA (2 つの最下位ビットが暗黙的に 0) を形成します。 フラグが 0 以外の場合、残りのビットはパックされたアンワインド データ構造体を形成します。"例外情報 RVA" は、
.xdata
セクションに格納されている可変長例外情報構造体のアドレスです。 このデータは、4 バイトでアラインされている必要があります。パックされたアンワインド データは、関数からのアンワインドに必要な演算の圧縮記述です (標準の形式を想定)。 この場合、
.xdata
レコードは必要ありません。
.xdata
誌
パックされたアンワインド形式では関数のアンワインドの記述に十分でない場合、可変長の .xdata
レコードを作成する必要があります。 このレコードのアドレスは、.pdata
レコードの第 2 ワードに格納されています。 .xdata
の形式は、パックされた可変長の単語セットです。
このデータは、次の 4 つのセクションに分かれています。
構造体の全体的なサイズを記述し、キー関数データを提供する 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 ビット フィールドです。
E が 0 の場合は、セクション 2 で説明されているエピローグ スコープの合計数を指定します。 32 個以上のスコープが関数に存在する場合、Code Words フィールドが 0 に設定され、拡張ワードが必要であることを示す必要があります。
E が 1 の場合、このフィールドは、エピローグのみを記述する最初のアンワインド コードのインデックスを指定します。
f. Code Words は、すべてのアンワインド コードをセクション 3 に含めるために必要な 32 ビット ワードの数を指定する 5 ビット フィールドです。 31 を超える単語 (つまり、124 個のアンワインド コード) が必要な場合は、拡張単語が必要であることを示すには、このフィールドを 0 にする必要があります。
g. Extended Epilog Count および Extended Code Words は、それぞれ 16 ビットおよび 8 ビットのフィールドです。 これらは、エピローグの数が通常よりも多い場合や、アンワインド コード ワードの数が通常よりも多い場合に、エンコーディング用の追加の領域を提供します。 これらのフィールドを含む拡張ワードは、1 番目のヘッダー ワードの Epilog Count と Code Words フィールドの両方が 0 の場合にのみ存在します。
エピローグの数が 0 でない場合は、エピローグ スコープに関する情報の一覧が、1 単語にパックされ、ヘッダーとオプションの拡張ヘッダーの後に表示されます。 これらは、開始オフセットが増加する順に格納されます。 各スコープには、以下のビットが含まれています。
a. Epilog Start Offset は、関数の開始を基準にしたエピローグの (バイト単位の) オフセットを 4 で除算した値を示す 18 ビット フィールドです。
b. Res は、将来の拡張用に予約された 4 ビット フィールドです。 この値は 0 である必要があります。
c. Epilog Start Index は 10 ビット フィールドです (Extended Code Words よりも 2 ビット多い)。 これは、このエピローグを記述する最初のアンワインド コードのバイト インデックスを示します。
エピローグ スコープのリストの後には、アンワインド コードを含むバイトの配列が続きます (詳細については後のセクションで説明します)。 この配列は、最も近いフルワード境界の末尾に埋め込まれます。 アンワインド コードは、この配列に書き込まれます。 これらは関数の本体に最も近いものから開始し、関数の端に向かって移動します。 各アンワインド コードのバイトはビッグ エンディアン順に格納されるため、最も重要なバイトが最初にフェッチされ、操作と残りのコードの長さが識別されます。
最後に、ヘッダーの 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_regp
、save_regp_x
、save_fregp
、save_fregp_x
、save_r19r20_x
、または別の save_next
)。 これは次のレジスタ ペアを "拡大" 順で次の 16 バイトスロットに保存します。 save_next
は、最後の Int レジスタ ペアを表す save-next
に続く場合、最初の FP レジスタ ペアを参照します。
通常の戻り命令とジャンプ命令のサイズは同じであるため、末尾呼び出しシナリオでは分離された end
アンワインド コードは必要ありません。
end_c
は、連続していない関数フラグメントを最適化のために処理するように設計されています。 現在のスコープ内のアンワインド コードの終了を示す end_c
の後に、実際の end
で終わる別の一連のアンワインド コードが続く必要があります。 end_c
とend
の間のアンワインド コードは、親リージョン ("ファントム" プロローグ) でのプロローグ操作を表します。 詳細と例については、以下のセクションで説明します。
パックされたアンワインド データ
関数のプロローグおよびエピローグが下に示す標準形式に従っている場合、パックされたアンワインド データを使用できます。 これにより、 .xdata
レコードの必要性が完全になくなり、アンワインド データを提供するコストが大幅に削減されます。 正規のプロローグとエピローグは、単純な関数の一般的な要件を満たすように設計されています。例外ハンドラーを必要とせず、標準の順序でセットアップと破棄の操作を行います。
パックされたアンワインド データを含む .pdata
レコードの形式は次のようになります。
フィールドは次のとおりです。
- Function Start RVA は、関数の開始の 32 ビット の RVA です。
- フラグ は、上記の 2 ビット フィールドで、次の意味を持ちます。
- 00 = パックされたアンワインド データは使用されません。残りのビットが
.xdata
レコードを指す - 01 = スコープの先頭と末尾で 1 つのプロローグとエピローグで使用される、パックされたアンワインド データ
- 10 = プロローグとエピローグのないコードに使用される、パックされたアンワインド データ。 分離された関数セグメントを記述する場合に便利です
- 11 = 予約済み。
- 00 = パックされたアンワインド データは使用されません。残りのビットが
- 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>
で使用されます
- 00 = 非チェーン関数、
- 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_fp
、 save_regp 0,240
、 save_fregp,0,224
、 save_fplr_x_256
、 end
エピローグは通常の順序であるため、単純です。 エピローグ内のオフセット 0 (関数内のオフセット 0x100から始まる) から開始すると、クリーンアップがまだ行われていないため、完全なアンワインド シーケンスが実行されます。 (エピローグのオフセット 2 で) 命令が 1 つある場合、最初のアンワインド コードをスキップして正常にアンワインドできます。 この状況を一般化し、オペコードとアンワインド コードの間に 1:1 のマッピングがあることを想定できます。 次に、エピローグで命令 n からアンワインドを開始するには、最初の n 個のアンワインド コードをスキップし、そこから実行を開始する必要があります。
これにより、(逆になることを除き) 同じようなロジックがプロローグに対して機能することがわかります。 プロローグ内のオフセット 0 からアンワインドを開始する場合は、何も実行しません。 1 つの命令があるオフセット 2 からアンワインドする場合は、末尾から 1 つのアンワインド コードのアンワインド シーケンスの実行を開始する必要があります。 (コードは逆の順序で格納されます。ここでも一般化できます。プロローグで命令 n からアンワインドを開始する場合は、コードの一覧の末尾から n アンワインド コードの実行を開始する必要があります。
プロローグ コードとエピローグ コードが常に正確に一致するとは限らないため、アンワインド配列には複数のコード シーケンスを含める必要がある場合があります。 コードの処理を開始する位置のオフセットを特定するには、次のロジックを使用します。
関数の本体内からアンワインドする場合は、インデックス 0 でアンワインド コードの実行を開始し、
end
オペコードに達するまで続行します。エピローグ内からのアンワインドの場合、エピローグ スコープにより開始ポイントとして提供されるエピローグ固有の開始インデックスを使用します。 エピローグの開始から問題の PC までのバイト数を計算します。 次に、アンワインド コードを前方に進め、既に実行済みの命令がすべて含まれるまでアンワインド コードをスキップします。 その後、その時点から実行します。
プロローグ内からアンワインドする場合は、開始点としてインデックス 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: すべてのエピローグは分離されたリージョンにあります):
プロローグだけを記述する必要があります。 このプロローグをコンパクトな
.pdata
形式で表すことはできません。 完全な.xdata
の場合は、Epilog Count = 0 を設定することで表すことができます。 上の例のリージョン 1 を参照してください。アンワインド コード:
set_fp
、save_regp 0,240
、save_fplr_x_256
、end
。エピローグのみ (リージョン 2: プロローグはホスト リージョンにあります)
コントロールがこの領域にジャンプする時点までに、すべてのプロローグ コードが実行されていると想定されます。 部分アンワインドが、通常の関数と同じようにエピローグで発生する可能性があります。 この種類の領域は、コンパクト
.pdata
で表すことはできません。 完全な.xdata
レコードでは、"ファントム" プロローグでエンコードし、end_c
とend
アンワインド コード ペアで角かっこで囲むことができます。 先頭のend_c
は、プロローグのサイズが 0 であることを示します。 単一のエピローグのエピローグ開始インデックスは、set_fp
を指しています。リージョン 2 のアンワインド コード:
end_c
、set_fp
、save_regp 0,240
、save_fplr_x_256
、end
。プロローグもエピローグもない (リージョン 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, 224
、end_c
、set_fp
、save_regp 0,240
、save_fplr_x_256
、end
。 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] は、プロローグ アンワインド コードの中間を指します (アンワインド配列を部分的に再利用します)。