Windows x64での例外。 どのように機能しますか? パート2

この記事の最初の部分で説明した資料に基づいて、Windows x64での例外処理のトピックについて引き続き説明します。 このパートでは、例外処理プロセスに関係するPEイメージの領域を詳細に調べます。 説明されている資料には、プロローグ、エピローグ、関数のフレームなどの基本概念の知識、およびプロローグとエピローグのアクション、関数パラメーターの転送、関数結果の戻りなどの基本プロセスの理解が必要です。 読者が上記に精通していない場合は、読む前にこの記事の最初の部分の資料をよく理解することをお勧めします。



このアドレスの gitリポジトリのexceptionsフォルダーにあるメカニズムの実装は、記事に添付されています。



1. PEイメージの.pdataセクション



このセクションには、PEイメージの機能の表と、これらの機能のフレームのプロモーションに関する情報が含まれています。 オペレーティングシステムは、例外ハンドラを検索するときにこのテーブルを積極的に使用します。 テーブルのサイズと場所は、PEイメージのデータディレクトリ(オプションのヘッダーデータディレクトリ)のオプションのヘッダーに記述されています。 次のセクションでは、機能の説明に含まれる構造について説明します。 これらの構造体のフィールドには、画像のさまざまな領域を指すアドレスを格納できます。 これらのアドレスはすべて、特に指定のない限り、画像の先頭からの相対アドレスです。



PEイメージのより詳細な説明は、Microsoft Portable Executable and Common Object File Format Specificationドキュメントにあります。 この記事では、議論中のトピックに直接関連する情報のみを提供します。



2.機能表



関数テーブルは、タイプRUNTIME_FUNCTIONの要素で構成され、その定義を図1に示します。









図1



BeginAddressとEndAddressにはそれぞれ関数の開始と終了のアドレスが含まれ、UnwindInfoAddressにはプロモーション情報構造のアドレスが含まれます。 EndAddressに関数の直後の最初のバイトのアドレスが含まれる場合、BeginAddressには関数の最初のバイトのアドレスが含まれます。 この構造は、関数全体ではなく、その一部(チャンク)のみを記述することもできます。 この状況については、セクション3で詳しく説明します。



テーブル要素は、関数/関数の一部の開始アドレスに従って、昇順でソートされます。



3.プロモーション情報



前述したように、RUNTIME_FUNCTION構造体のUnwindInfoAddressフィールドには、UNWIND_INFO構造体のアドレスが含まれています。その定義を図2に示します。









図2



バージョンフィールドは、名前が示すように、プロモーション情報のバージョンを保持します。 現在、最新バージョンはバージョン2です。



SizeOfPrologフィールドには、プロローグサイズがバイト単位で含まれています。



FrameRegisterフィールドは、フレームポインターとして使用されるレジスタ番号(レジスタ番号は以下にリストされます)を示し、FrameOffsetフィールドには16バイトブロックの値が含まれ、関数フレームポインターが設定されるときにRSP値に追加されます。 実際、フレームポインターはRSP + FrameOffset * 16に設定されます。結果のオフセット値は0〜240です。このフレーム内オフセットを使用すると、コード密度を上げることができます。 この場合、8ビットの符号付き値をオフセットとして使用する、より短い命令を使用できます。 たとえば、FrameOffsetが0の場合、フレームデータの最初の128バイトへのアクセスは短い命令を使用して実行できますが、後続のデータへのアクセスはありません。 8ビットの符号付きの値は、0から127までの正の値をカバーします。FrameOffsetが20であると想像してください。 オフセットとして、0から127の値だけでなく、-20から0の値も使用できます図3は、同じオフセットで異なるFrameOffset値を持つフレームへのアクセスの2つの例を示しています。 どちらの場合でも、フレームにアクセスする命令のサイズに注意してください。









図3



FrameRegisterがゼロの場合、フレームポインターは使用されず、FrameOffsetフィールドには情報が含まれません。 この場合、フレームインジケータはRSPです。



UnwindCode配列は、プロローグのアクションを記述します。 これらのコードの詳細な説明は、セクション4にあります。アライメントのために、この配列には常に偶数のレコードがあります。 CountOfCodesフィールドには、この配列の要素の数が含まれています。 この数が奇数の場合、配列の最後のレコードは使用されません。つまり、実際には、配列内のレコードの数はフィールドに示されている数よりも1つ多くなります。 配列要素は逆の順序でソートされます。 最初の要素は、プロローグの最後のアクションを記述します。



Flagsフィールドには、UNW_FLAG_EHANDLER、UNW_FLAG_UHANDLER、UNW_FLAG_CHAININFOの3つのフラグを含めることができます。 UNW_FLAG_EHANDLERフラグは、ハンドラーの検索中に呼び出す必要がある例外ハンドラーが関数にあることを示します。 UNW_FLAG_UHANDLERフラグは、機能にプロモーション中に呼び出す必要がある終了ハンドラーがあることを示します。 フラグUNW_FLAG_CHAININFOは、このUNWIND_INFO構造がプライマリではなく、以前のUNWIND_INFO構造の継続(連鎖)であることを意味します。 このフラグを設定すると、UNW_FLAG_EHANDLERおよびUNW_FLAG_UHANDLERフラグの設定が除外されます。FrameRegisterおよびFrameOffsetフィールドは、プライマリUNWIND_INFO構造体のフィールドと同一である必要があります。



UNWIND_INFO構造の直後に、EXCEPTION_HANDLER構造またはRUNTIME_FUNCTION構造が配置されます。 EXCEPTION_HANDLERの構造は、図4で定義されています。









図4



Flagsフィールドに設定されたUNW_FLAG_EHANDLERビットまたはUNW_FLAG_UHANDLERビット、またはその両方が含まれている場合、EXCEPTION_HANDLER構造はUNWIND_INFO構造に従います。 ExceptionHandlerAddressフィールドには、呼び出されたハンドラーのアドレスが含まれ、LanguageSpecificDataフィールドには、対応するプログラミング言語に固有のデータが含まれます。 ハンドラー関数のプロトタイプと、それによって返される値のタイプを図5に示します。









図5



ExceptionRecordパラメーターは、例外の理由を説明する構造体へのポインターを保持します。 EstablisherFrameパラメーターは、ハンドラーが呼び出されたフレームへのポインターを保持します。 ContextRecordパラメーターには、例外発生時のプロセッサーコンテキストを含む構造体へのポインターが含まれます。 例外ハンドラーまたはスタックプロモーションを検索するプロセスでは、ハンドラー自体によって構造の内容を変更できます。 これらの変更の結果は、プロセッサのターゲットコンテキスト、つまり このコンテキストは、実行が継続する場合、進行中のタスクに適用されます。 DispatcherContextパラメーターには、例外またはスタックプロモーションハンドラーの現在の検索コンテキストが含まれます。 構造体EXCEPTION_RECORDとDISPATHCER_CONTEXT、およびハンドラーとプロモーションの検索プロセスについては、この記事の次のパートで詳しく説明します。 EXCEPTION_DISPOSITIONタイプについても、このプロセスの説明で説明します。 EXCEPTION_RECORD、CONTEXT、およびDISPATHCER_CONTEXT構造体の定義は、winnt.hまたは記事に添付されたこのメカニズムの実装(それぞれSExceptionRecord、SContext、およびSDispatcherContextの名前)にあります。



図6は、特定の関数に対してコンパイラーが生成する構造RUNTIME_FUNCTION、UNWIND_INFO、およびEXCEPTION_HANDLERの例を示しています。 破線は関数の終わりを示しています。 EXCEPTION_HANDLER構造は、説明を完全にするために図に示されており、UNWIND_INFO構造の一部ではありませんが、この構造の一部として図が示されています。 存在する場合は、すでに示したように、UNWIND_INFO構造体の直後に続きます。 画像の先頭、関数の先頭、および関数の末尾のアドレスは絶対アドレスであり、生成された構造内のすべてのアドレスは画像の先頭からの相対アドレスです。









図6



Flagsフィールドに設定されたUNW_FLAG_CHAININFOビットが含まれている場合、UNWIND_INFO構造はセカンダリ(チェーンとも呼ばれます)であり、その後にRUNTIME_FUNCTION構造が続きます。 RUNTIME_FUNCTION構造体のUnwindInfoAddressフィールドには、以前のUNWIND_INFO構造体のアドレスが含まれています。 同様に、以前のUNWIND_INFO構造もセカンダリになる可能性がありますが、最終的にはUNW_FLAG_CHAININFOフラグが設定されていないUNWIND_INFO構造がこのリンクリストになります。 これは、一次構造とも呼ばれる関数エントリポイントのプロローグに属する構造になります。 関連する構造の数は、最大32までです。



関連する構造は、2つの状況で役立ちます。



最初の状況:コンパイラーは最適化を実行でき、その結果、いくつかの定数レジスターの保存を遅らせる可能性があります。 つまり それらの保存は、関数エントリポイントのプロローグではなく、関数本体で実行されます。 このようなコードのセクション(セクション2で、RUNTIME_FUNCTION構造体は関数全体ではなく、その一部のみを記述できるということは既に言及しました)コンパイラは、ストレージを記述する対応するUNWIND_INFO構造体を指すRUNTIME_FUNCTION構造体を生成しますこれらのレジスタ。 この場合、これらのレジスタの保存はメモリへの定期的な書き込みによって実行されます。 この状況では、スタックへのプッシュはサポートされていません。 前述のように、このUNWIND_INFO構造はセカンダリであり、RUNTIME_FUNCTION構造が続きます。これには、以前のUNWIND_INFO構造またはプライマリUNWIND_INFO構造のアドレスが含まれます。



2番目の状況:最初の状況から、関連付けられたUNWIND_INFO構造を介して、プロモーション情報のサイズを削減できます。 プライマリおよび/または以前のUNWIND_INFO構造からプロモーションコードの配列を複製する必要はありません。



図7は、前述の最適化が適用される関数に対してコンパイラーが生成するRUNTIME_FUNCTIONおよびUNWIND_INFO構造の例を示しています。 破線は関数の終わりを示しています。 UNWIND_INFO構造に続くRUNTIME_FUNCTION構造は、これらの構造の一部ではないにもかかわらず、それらの一部として表されます。 それらの存在は、UNWIND_INFO構造体のフィールドに依存します。 図に個別に示されているRUNTIME_FUNCTION構造体は、関数テーブルの要素であり、セクション2で説明されているように、この関数の各部分の開始アドレスに従って昇順でテーブル内でソートされます。 図では、下から上に昇順で表示されています。 それぞれの下には、それらが参照するUNWIND_INFO構造があります。 最下位のRUNTIME_FUNCTION構造は、プライマリUNWIND_INFO構造を参照します。 残りは、セカンダリのUNWIND_INFO構造を参照します。 したがって、これらのUNWIND_INFO構造体の後に、RUNTIME_FUNCTION構造体が配置されます。これらの構造体は、コンテンツ内で最も低いRUNTIME_FUNCTION構造体を完全に繰り返し、プライマリUNWIND_INFO構造体を参照します。 また、この例からわかるように、コンパイラによって生成されたコードは、RDIレジスタを関数エントリポイントのプロローグではなく、関数本体に保存します。 コードのこの部分は、中央のRUNTIME_FUNCTION構造体によって記述され、対応するUNWIND_INFO構造体には、このコードセクションのプロローグに典型的な対応するエントリが含まれています。 画像の先頭、関数の先頭、および関数の末尾のアドレスは絶対アドレスであり、生成された構造内のすべてのアドレスは画像の先頭からの相対アドレスです。









図7



Flagsフィールドに設定ビットが含まれていない場合、UNWIND_INFO構造に続く構造はありません。



4.プロモーションコード



前のセクションでは、プロローグのアクションを説明するUNWIND_INFO構造体のUnwindCode配列について説明しました。 ここでは、図8に示すこの配列の要素の構造と、プロローグアクションの説明で使用される各コードについて検討します。









図8



図からわかるように、要素の内容は、状況に応じて、3つのオプションのいずれかで解釈されます。



上部構造は、プロローグの動作を説明するために使用されます。



CodeOffsetフィールドには、プロローグの先頭から、説明されたアクションを実行する命令の後の命令までのオフセットが含まれます。



OpCodeフィールドには、実行されているアクションのコードが含まれています。 異なるアクションは、1から3までの異なる数のテーブルエントリを取ります。最初のレコードは常に最上位構造の形式です。 残りのエントリの形式と数は、アクションコードによって異なります(すべてのコードについては以下で説明します)。 追加のレコードが1つしかない場合、このレコードはFrameOffsetフィールドとして解釈されます。 追加のレコードが2つある場合、これらのレコードもFrameOffsetフィールドとして解釈されますが、最初のレコードは32ビット値の下位16ビット、2番目の上位16ビットを保持します。 これらのレコードのバイト順は直接(リトルエンディアン)です。 これらの値の目的(1つまたは2つの追加エントリの場合)は、対応するアクションコードで説明されています。 OpCodeフィールドにUWOP_EPILOGコードが含まれている場合、構造の形式はEpilogue構造になります。 この状況については、後で詳しく説明します。 このコードは、UNWIND_INFOバージョン2構造にのみ関連することに注意してください。



OpInfoフィールドは、OpCodeフィールドの値に依存します。 アクションに関与する汎用レジスタの番号、レジスタのXMM番号、または対応するアクションコードで目的が説明されている数値を含めることができます。 ここにリストされていないOpInfoフィールドを解釈するためのオプションは、対応するアクションコードで説明されています。 図9に、OpInfoフィールドの値と汎用レジスタおよびXMMレジスタのマッピングを示します。









図9



一部のアクションコードには、関数フレーム内のメモリ領域に符号なしオフセットが含まれています。 このオフセットは、関数フレームの開始からの相対です。 関数フレームポインターが使用されていない場合、これはRSP値に対するオフセットです。 フレームポインターが使用されている場合、これはフレームポインターが設定された時点のRSP値に対するオフセットです。 このRSPの値は、フレームポインター-UNWIND_INFO構造体からのFrameOffset * 16に等しくなります。フレーム内のすべてのオフセットは、実行されるアクションのコードに応じて、8または16の倍数です。 UWOP_SAVE_NONVOLおよびUWOP_SAVE_NONVOL_FARの場合、オフセットは8の倍数です。 これらのコードは8バイトのレジスタを保存します。 UWOP_SAVE_XMM128およびUWOP_SAVE_XMM128_FARの場合、オフセットは16の倍数です。 これらのコードは16バイトのレジスタを保存します。 この記事の最初のセクションのセクション1で述べたように、関数にフレームポインターがある場合、汎用レジスターとXMMレジスターはフレームポインターの設定後に保存されるため、フレーム領域のオフセットを含むプロモーションコードは常にUWOP_SET_FPREGプロモーションコードの前のUnwindCode配列。



有効なコードはすべて以下に説明されています。 コードの説明は名前から始まり、括弧内はそれぞれの数値と占有するエントリ数です。



UWOP_PUSH_NONVOL(0; 1エントリ)。 汎用レジスターをスタックにプッシュし、RSP値を8減らします。レジスター番号はOpInfoフィールドに示されます。 この記事の前半のセクション1で説明したように、プロローグは最初にこれらのアクションを正確に実行するため、これらのコードはUnwindCode配列の最後に表示されます。



UWOP_ALLOC_LARGE(1; 2または3エントリ)。 スタック上の広い領域を割り当てます。 このコードには2つの形式があります。 OpInfoが0の場合、割り当てられたサイズを8で割った値が次のレコードに格納され、最大512Kb-8を割り当てることができます。OpInfoが1に等しい場合、割り当てられたサイズは次の2つのエントリに格納され、最大4GB-8を割り当てることができます



UWOP_ALLOC_SMALL(2; 1エントリ)。 スタック上の小さな領域を割り当てます。 割り当てられたサイズはOpInfoフィールドに保存され、次のように計算されます-OpInfo * 8 +8。これにより、8〜128バイトを選択できます。



スタック上の領域を割り当てるプロモーションコードは、常に最短形式のエンコードを使用します。 8〜128バイトの領域が割り当てられている場合、UWOP_ALLOC_SMALLが使用されます。 136バイトから512Kb-8までの領域が割り当てられている場合、OpInfoフィールド値0でUWOP_ALLOC_LARGEが使用されます。512KBから4GB-8までの領域が割り当てられている場合、OpInfoフィールド値1でUWOP_ALLOC_LARGEが使用されます



UWOP_SET_FPREG(3; 1エントリ)。 フレームポインターを設定します。 OpInfoフィールドは予約されており、使用されていません。UNWIND_INFO構造体のFrameRegisterフィールドについて説明する場合、プロセス自体はセクション3で詳細に説明されています。



UWOP_SAVE_NONVOL(4; 2エントリ)。 メモリに書き込むための命令とともに、汎用レジスターをスタックに格納します。 値は、以前に選択した領域に保存されます。 格納されているレジスタの番号はOpInfoで指定されます。 8で割ったフレームの先頭からのオフセットは、次のレコードに格納されます。



UWOP_SAVE_NONVOL_FAR(5; 3エントリ)。 長いオフセットを使用して、メモリ書き込み命令とともに、汎用レジスターをスタックに保存します。 値は、以前に選択した領域に保存されます。 格納されているレジスタの番号はOpInfoで指定されます。 フレームの先頭からのオフセットは、次の2つのエントリに格納されます。



UWOP_EPILOG(6; 2エントリ)。 UNWIND_INFO構造のバージョン1の場合、このコードはUWOP_SAVE_XMMと呼ばれ、2つのレコードを占有し、XMMレジスターの下位64ビットを保持していましたが、後で削除されてスキップされました。 実際には、このコードは使用されていません。 UNWIND_INFO構造のバージョン2の場合、このコードはUWOP_EPILOGと呼ばれ、2つのエントリを取り、関数エピローグを説明します。 このコードの詳細な説明を以下に示します。



UWOP_SPARE_CODE(7; 3エントリ)。 UNWIND_INFO構造のバージョン1の場合、このコードはUWOP_SAVE_XMM_FARと呼ばれ、3つのレコードを占有し、XMMレジスターの下位64ビットを保持していましたが、後で削除されてスキップされました。 実際には、このコードは使用されていません。 UNWIND_INFO構造のバージョン2の場合、このコードはUWOP_SPARE_CODEと呼ばれ、3つのエントリを取りますが、意味がありません。



UWOP_SAVE_XMM128(8; 2エントリ)。 スタック上のXMMレジスタの128ビットすべてを保存します。 格納されているレジスタの番号はOpInfoで指定されます。 16で割ったフレームの先頭からのオフセットは、次のレコードに格納されます。



UWOP_SAVE_XMM128_FAR(9; 3エントリ)。 スタック上のXMMレジスタの128ビットすべてを保存します。 格納されているレジスタの番号はOpInfoで指定されます。 フレームの先頭からのオフセットは、次の2つのエントリに格納されます。



UWOP_PUSH_MACHFRAME(10; 1エントリ)。 機械フレームを押します。 レコードは、ハードウェア割り込みまたは例外のアクションを示すために使用されます。 このコードには2つの形式があります。 OpInfoが0の場合、これはプロセッサが次のレジスタをスタックに順次プッシュしたことを意味します:SS、古いRSP、EFLAGS、CS、RIP。 OpInfoが1の場合、これは、プロセッサがOpInfoが0であるのと同じレジスタをスタックにプッシュしたが、プッシュする前にエラーコードをスタックにプッシュしたことを意味します。 プッシュ後の各値は8の倍数のアドレスにあります。値が8バイト未満の場合、古い未使用バイトはリセットされます。 このコードを使用する場合、UnwindCode配列では最後に表示されます。 OpInfoが0の場合、RSP値は40減少します。それ以外の場合は48減少します。図10は両方のケースを示し、矢印はスタック成長の方向を示します。





図10



図11は、この記事の最初の部分で図1に示したプロローグのUnwindCode配列の例を示しています。





図11



図12は、この記事の最初の部分で図3に示したプロローグのUnwindCode配列の例を示しています。 UWOP_SET_FPREGコードの場合、必要な情報はすべてUNWIND_INFO構造体のFrameRegisterフィールドとFrameOffsetフィールドにあることに注意してください。





図12



ここで、UWOP_EPILOGを詳細に検討してください。



前述のように、このレコードはUNWIND_INFOバージョン2構造にのみ存在し、図8に示すエピローグ構造の形式を持っています。このコードは、関数エピローグの場所を記述しています。 これにより、この記事の第1部のセクション1で説明したように、UnwindCode配列から、プログラムコードではなく、割り込み/例外発生時にプロセッサがエピローグコードを実行したかどうかを判断できます。



関数は複数のエピローグを持つことができるため、各エピローグには1つのUWOP_EPILOGエントリがあります。 UWOP_EPILOGコードが使用される場合、UnwindCode配列でこのエントリが最初に表示され、その後に少なくとも1つ以上のUWOP_EPILOGエントリが続きます。 OffsetLowOrSizeフィールドの最初のUWOP_EPILOGエントリは、エピローグのサイズを示します。 最初のUWOP_EPILOGレコードのOffsetHighOrFlagsフィールドのビット0が設定されている場合、OffsetLowOrSizeフィールドはサイズだけでなく、関数エピローグへのオフセットでもあります。これは、エピローグが関数の最後/関数の一部にある場合のみ可能です。 これらのエントリのエピローグによるオフセットは反対です。 関数の開始アドレス/関数の一部には追加されませんが、エピローグの開始アドレスを計算するために、関数の終了アドレス/関数の一部から減算されます。 セクション2で既に述べたように、関数の開始および終了/関数の一部のアドレスは、RUNTIME_FUNCTION構造体のBeginAddressおよびEndAddressフィールドに含まれています。 最初のUWOP_EPILOGレコードのOffsetHighOrFlagsフィールドのビット0が設定されていない場合、このレコードの後に​​次のUWOP_EPILOGレコードが続きます。このレコードのOffsetLowOrSizeおよびOffsetHighOrFlagsフィールドには、それぞれ下位8ビットと最上位4オフセットビットが含まれます。 既に述べたように、各関数エピローグには追加のUWOP_EPILOGレコードがあります。この場合、前の例のように、OffsetLowOrSizeおよびOffsetHighOrFlagsフィールドは、関数の最後/関数の一部から12ビットのオフセットを形成します。



なぜなら UWOP_SAVE_XMMコードは2つのレコードを使用します。UWOP_EPILOGが使用するレコードの数は常に偶数であり、最後のレコードは使用できません。 この場合、UWOP_EPILOGエントリのOffsetLowOrSizeおよびOffsetHighOrFlagsフィールドは0です。



図13は、関数の末尾にエピローグが1つある関数のUnwindCode配列の例を示しています。 関数の開始と終了のアドレスは絶対アドレスです。





図13



図13に示すように、関数がスタックのプロローグに領域を割り当てるという事実にもかかわらず、エピローグの先頭は、メモリがスタックから解放されるのではなく、汎用レジスターがスタックからプッシュされる場所です。 これについては、この記事の次のパートで説明します。



これは、UWOP_EPILOGコードの概念に組み込まれています。 エピローグは、汎用レジスタをプッシュするための命令と、RSPを8増やす命令(この記事の第1部のセクション1で既に述べたとおり)、および戻り命令で構成できます。



図14は、3つのエピローグを持つ関数のUnwindCode配列の例を示しています。 関数の開始と終了のアドレスは絶対アドレスです。





図14



おわりに



記事のこの部分では、必要な理論資料の検討を終了しました。 この記事の次のパートでは、 PEイメージ構造の操作を簡素化するためにオペレーティングシステムに導入される補助機能と構造について説明します。 次に、例外を発生させて処理するプロセスと、このプロセスでこれらのヘルパー関数がどのように使用されるかを見ていきます。



All Articles