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

この記事の第1部と第2部で説明した資料に基づいて、Windows x64での例外処理のトピックについて引き続き説明します。



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



与えられた説明はWindowsでの実装を参照しているため、概念的に違いはありませんが、記事に添付されているこのメカニズムの実装が完全に一致するとは想定しないでください。 明示的に述べられていない限り、添付の実装の詳細はこの記事では考慮されません。 したがって、これらの詳細は、必要に応じて個別に検討する必要があると想定されています。



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



1.例外とその処理



次のサブセクションでは、例外処理とその基礎となるすべてのものについて詳細に説明します。 与えられた説明はWindowsでの実装を参照しているため、概念的に違いはありませんが、記事に添付されているこのメカニズムの実装が完全に一致するとは想定しないでください。 明示的に述べられていない限り、添付の実装の詳細はこの記事では考慮されません。 したがって、これらの詳細は、必要に応じて個別に検討する必要があると想定されています。



1.1。 ヘルパー関数



例外処理のプロセスの説明を始める前に、カーネルスペースのntoskrnl.exeモジュールとユーザースペースのntdll.dllライブラリによってエクスポートされるRtlLookupFunctionEntry関数とRtlVirtualUnwind関数を検討する必要があります。



RtlLookupFunctionEntry関数(そのプロトタイプは図1に示されています)は、RUNTIME_FUNCTION構造体へのポインターと、ControlPcパラメーターで渡されるアドレスのコードに対応するPEイメージの開始アドレスを返します。









図1



ImageBaseパラメーターは変数へのポインターを受け取り、関数はPEイメージの先頭のアドレスを返します。HistoryTableパラメーターはオプションで、検索キャッシュに使用される構造体へのポインターを受け取ります。 最後のパラメーターの構造形式は、winnt.hにあります。 関数がNULLを返した場合、渡されたコードポインターで対応するPEイメージが見つからなかったか、対応するレコードが関数テーブルで見つかりませんでした。これは、関数にフレームがないことを意味します。



図2にプロトタイプが示されているRtlVirtualUnwind関数は、関数の仮想プロモーションを実行します。









図2



この関数は物理プロセッサの状態を変更せず、代わりに特定の時点でのプロセッサコンテキストを記述する構造体のContextRecordパラメーターのパラメーターを取るため、仮想と呼ばれます。 昇格後のプロセッサコンテキストは同じ構造に戻ります。 CONTEXT構造自体を図3に示します。









図3



フィールドP1Home-P6Homeは、構造体を使いやすくするために導入されています。たとえば、レジ​​スタおよびスタックパラメータの領域として使用できます。 ContextFlagsフィールドはビットフィールドであり、構造全体の状態、つまり、 どのフィールドが対応するプロセッサレジスタの状態を反映し、どのフィールドは反映しません。 フィールドには次のフラグが含まれる場合があります。





ContextFlagsフィールドはRtlVirtualUnwind関数によって解釈されることはなく、常に特定の瞬間における構造の実際の状態が構造に含まれていることを前提としています。 フィールドVectorRegister、VectorControl、DebugControl、LastBranchToRip、LastBranchFromRip、LastExceptionToRip、およびLastExceptionFromRipは、議論中のトピックに直接関連していないため、ここではそれらの目的について説明しません。 FltSaveフィールドは、FPUの状態を含む完全なXMMコンテキストの状態を維持する必要がある場合に使用されます。



ImageBaseパラメーターは、コードが実行されていたPEイメージのアドレスを受け入れます。 ControlPcパラメーターは、実行が中断された命令のアドレスを保持し、FunctionEntryパラメーターは、命令のアドレスに対応するRUNTIME_FUNCTION構造体のアドレスを受け入れます。



HandlerTypeパラメーターは、予期されるハンドラーのタイプを受け入れます。 値UNW_FLAG_EHANDLERが受け入れられ、ツイストされていない関数に例外ハンドラがある場合、関数はこの例外ハンドラのアドレスを返します。 値UNW_FLAG_UHANDLERが受け入れられ、ツイストされていない関数にプロモーションハンドラーがある場合、関数はこのプロモーションプロセッサのアドレスを返します。 それ以外の場合、関数はNULLを返します。 また、プロローグコードまたはエピローグコードが実行されたときに、ねじれのない関数の実行が中断された場合、関数はNULLを返すことに注意してください。 ここでの哲学は、例外および/または昇格ハンドラーが関数の本体にバインドされるということです。 関数がハンドラーの1つのアドレスを返す場合、対応するプログラミング言語のコンパイラーによってこのハンドラーに関連付けられているデータのアドレスも返します。 アドレスは、HandlerDataパラメーターで渡されたポインターによって返されます。 この記事の第2部のセクション3で説明したように、プロセッサに関連付けられたデータが同じ構造のLanguageSpecificDataフィールドに格納されている場合、例外ハンドラーおよび/またはプロモーションハンドラーのアドレスはEXCEPTION_HANDLER構造体のExceptionHandlerAddressフィールドに格納されます。



EstablisherFrameパラメーターは、関数がアンワインドする前に関数がフレームポインターを返す変数へのポインターを受け取ります。 ContextPointersパラメーターはオプションであり、使用される場合、構造のアドレスが含まれます。これは、昇格後に汎用レジスターとXMMレジスターの内容を繰り返します。 プロモーションに参加したレジスターのみが構造体に入ることに注意してください。



以下の図4に、関数の例を示します。









図4



左側は、wmain、func2、およびfunc1の3つの関数を含むサンプル画像です。 wmain関数はfunc2を呼び出し、次にfunc2がfunc1を呼び出します。 中央には、各関数のアセンブラー表現が表示されます。メモリ内の命令の絶対アドレスは左側に表示され、中央には命令のアセンブラーニーモニックコードが右側にある場合の、命令が実行される前のRSP値が表示されます。 RSP値は、すべての命令については表示されませんが、実行された命令またはスレッドの実行が中断された命令についてのみ表示されます。 関数のアセンブラ表現の右側には、対応する関数を呼び出す前にプロセッサレジスタの意味が表示されます;簡潔にするために、汎用レジスタと命令ポインタレジスタ(RIP)のみが表示されます。 ラベルは、関数に渡されるか、関数によって返されるパラメーターの値を示します。



RtlVirtualUnwind関数は、1つの関数、つまり 関数が実行されると、CONTEXT構造体の内容は、ツイストされていない関数が呼び出されなかったかのようになりますが、例外として、揮発性レジスタの値は復元されず、RIPには、対応する関数を呼び出す命令へのポインターが含まれず、その直後の命令に含まれます また、RtlVirtualUnwind関数は、ねじれていない関数のフレームポインターを返し、必要に応じて、そのハンドラーへのポインターとこのハンドラーのデータへのポインターを返します。



例に示されているように、ImageBaseパラメーターは0x7FF6AEAF000(ラベル1)です。 ControlPcパラメーターは0x7FF6AEAF104E(ラベル2)です。 FunctionEntryパラメーターには、ControlPcパラメーターで値が渡される命令のアドレスに対応するRUNTIME_FUNCTION構造体(ラベル3)のアドレスが含まれます。 ContextRecordパラメーターには、中断された関数のレジスタ値が含まれます(ラベル4)。 RUNTIME_FUNCTION構造体へのポインターは、R​​tlLookupFunctionEntry関数を使用して取得できます。 実行後の関数RtlVirtualUnwindは以下を返します。昇格後のレジスタの値(ラベル5)。 EXCEPTION_HANDLER構造からは、HandlerDataパラメーターのLanguageSpecificDataフィールドのアドレスを返し、同じ構造(ラベル6)のExceptionHandlerAddressフィールドから抽出されたハンドラーへのポインターを返します。 また、関数は、ツイストされていない関数のフレームポインターをEstablisherFrameパラメーターに返し、その値は0x9C5DBCF900になります。



昇格前のRtlVirtualUnwind関数は、プロセッサがプロローグ、エピローグ、またはツイストされていない関数の本体を実行したかどうかを決定します。 本体の場合、昇格はUNWIND_INFO構造を使用して実行されます。 プロローグである場合、プロローグも中断されます。ただし、プロローグが中断された場所を最初に判別し、プロモートを実行するのはこの場所からです。 エピローグの場合、UNWIND_INFO構造のバージョン2の場合、そこからプロモーションが実行されます。 また、プロローグの場合のように、プロモーションの前に、関数はエピローグの実行が中断された場所を決定し、この場所からプロモーションが実行されます。 UNWIND_INFO構造のバージョン1の場合、エピローグの後続の命令を分析することにより、プロモーションが実行されます。 このバージョンのUNWIND_INFO構造には、エピローグに関する情報は含まれていません。 この記事の第1部のセクション1では、UNWIND_INFOバージョン1構造によって記述された関数のエピローグの始まりは、 rsp、constantまたはlea rsp、[フレームポインター+定数]命令の追加と見なされると述べました。 実際、XMM命令の分析はプロモーション機能のコードを複雑にし、条件付きエピローグの前に例外が発生した場合、それらの値はUNWIND_INFO構造から復元されるため、これらのレジスタの整合性はプロモーション後に破損しません。 唯一の副作用は、XMMレジスタが復元されたときに例外が発生した場合です。この場合、RtlVirtualUnwind関数は例外ハンドラーへのポインターを返すため、例外が処理されるときに呼び出されます。 UNWIND_INFOバージョン2構造で記述された関数については、この哲学が少し変更され、汎用レジスタをスタックからプッシュするための命令がエピローグの始まりと見なされ始めました。 rsp命令、constant、lea rspを追加します。[フレームポインター+定数]は原則として例外を発生させることはできません。 図5にコードの例を示します。その右側には、2つの関数func1とfunc2のアセンブラー表現があります。 命令のアドレスは絶対的であり、簡潔にするために16進表現はありません。 例として、func1関数のスタックプロモーションを検討します。 図には、A、B、Cの3つのケースが示されています。それぞれが機能の状態を示しています。A-プロローグ、B-ボディ、C-エピローグ。









図5



以下の図6〜8では、各ケースが個別に考慮されます。 左側はスピン前のプロセッサレジスタで、右側はスピン後です。 コンパクトにするために、汎用レジスタと命令ポインタレジスタ(RIP)のみが示されています。 説明されている場合、これらのレジスタのみが変更される可能性があります。



ケースAを図6に示します。このケースでは、プロセッサはfunc1関数のプロローグを実行しました。この関数の実行は、RDIレジスタのプッシュ命令によって中断されました。 この場合のスタックプロモーションは、UNWIND_INFO構造体のデータを使用して実行されます。 最初に、RSIレジスタの値が復元され、次に戻りアドレスが復元されます。 したがって、レジスタRSI、RSP、RIPが変更されます。









図6



ケース7を図7に示します。このケースでは、プロセッサーはfunc1関数本体を実行しました。この関数の実行は、スタックの最上部からRAXレジスターへの8バイトの読み取り命令によって中断されました。 この場合のスタックプロモーションは、UNWIND_INFO構造体のデータを使用して実行されます。 最初に、ローカル関数変数のプロローグによって割り当てられたスタックからメモリが解放され、次にRDIおよびRSIレジスタの値が復元され、次に戻りアドレスが復元されます。 したがって、レジスタRSI、RDI、RSP、RIPが変更されます。









図7



ケース8を図8に示します。このケースでは、プロセッサはfunc1関数のエピローグを実行しましたが、その実行はRDIレジスタのプッシュ命令によって中断されました。 UNWIND_INFO構造体のバージョンに応じて、バージョン2構造体の場合は構造体自体を使用するか、バージョン1構造体の場合はエピローグコードを分析することにより、昇格が実行されます。最初に、RDIおよびRSIレジスタの値が復元され、次に戻りアドレスが復元されます。 したがって、レジスタRSI、RDI、RSP、RIPが変更されます。









図8



ケースAのControlPcパラメーターは0x7FF70C131036、ケースB-0x7FF70C13104E、ケースC-0x7FF70C131078です。 3つのケースすべてのImageBaseパラメーターは0x7FF70C130000になります。



ケースAのEstablisherFrameパラメーターは0x6DE73AF6E0、ケースBの場合は0x7E68EFF860、ケースCの場合は0x979BB9FAD8です。 3つのケースすべてで、これは昇格前のRSPレジスタの値になります。 別途、フレームポインターを持つ関数のEstablisherFrameの値を考慮してください。 図9は、命令のアドレスが絶対であり、命令が実行される前にスタックポインター(RSP)が表示されるような関数の例を示しています。









図9



命令がアドレス0x7FF6D76C1101で実行される前に機能が中断された場合、EstablisherFrameは値0x9E84B9FAA0を想定します。 命令がアドレス0x7FF6D76C110Aで実行される前に実行が中断された場合、EstablisherFrameは値0x9E84B9FA90を想定します。 命令がアドレス0x7FF6D76C1118で実行される前に実行が中断された場合、EstablisherFrameも値0x9E84B9FA90を取ります。 この例のように関数にフレームポインターがあり、関数が中断される前にインストールされた場合、EstablisherFrameは、現在のスタックポインターではなく、フレームポインターが設定された時点のスタックポインターの値を取得することに注意してください。 この例では、フレームポインターのインストールは、アドレス0x7FF6D76C1106の命令によって実行されました。



2.処理



例外処理のプロセス全体は、条件付きで2つの部分に分けることができます。



最初の部分は、例外ハンドラを見つけて呼び出すことです。 この部分は、オペレーティングシステムによって実行されます。 例外処理の概念図を図10に示します。





図10



上の図は、プロセス全体のスペースを示しています。 ユーザースペースは左側に表示され、カーネルスペースは右側に表示されます。 各スペースにはモジュールが含まれています。 どのアプリケーションでも、ntdll.dllモジュールは常に表示され、ユーザースペースに必要な補助タスクを実行します。 カーネルスペースには、Windowsカーネルであるntoskrnl.exeが常に存在します。 残りのモジュールは、例としてのみ示されています。 例外が発生すると、プロセッサは対応するゲートウェイ記述子から関数を呼び出します。ゲートウェイ記述子は、割り込み記述子テーブルの要素です。 この関数はカーネル関数です。 割り込みテーブルの詳細については、Intel 64およびIA-32アーキテクチャソフトウェア開発者マニュアルを参照してください。 次に、この関数は、KiExceptionDispatch関数とともに、処理に必要なすべてのデータを準備します。その後、KiDispatchException関数が呼び出され、処理前に追加のアクションが実行されます。1つは、ユーザー空間で例外が発生した場合、この例外の処理がユーザーにリダイレクトされることですスペース。 ntdll.dllモジュールは、ユーザー空間での処理を担当します。 処理に必要な準備がすべて完了すると、RtlDispatchException関数が呼び出され、イメージの.pdataセクションをスキャンしてハンドラーを検索して呼び出します。ハンドラーが見つかった場合、関数はそれを呼び出します。 関数はスタックをスピンせず、ハンドラー検索のみを実行することにも注意してください。



2番目の部分は、対応するプログラミング言語のコンパイラーによって生成されたEXCEPTION_HANDLER構造のLanguageSpecificDataフィールドの形式と、このフィールドに依存する検出された例外ハンドラーの実装に依存します。



この記事では、C / C ++言語のtry / exceptおよびtry / finallyコンストラクトを検討するため、2番目の部分の説明では、これらのコンストラクト用にコンパイラーによって生成されるEXCEPTION_HANDLER構造体のLanguageSpecificDataフィールドの形式を扱います。



次のサブセクションでは、ハンドラーの準備と検索のプロセス全体をさらに詳しく調べます。 説明を明確にし、簡略化するために、ゼロ除算例外を処理する例ではプロセス全体が考慮され、この例外を生成したコードはカーネルモードコードになります。 説明全体が特定の例外の例に限定されるという事実にもかかわらず、説明は他の例外にも関連します。 ゼロによる除算を排除する動作を繰り返さない場合、それらは非常に類似しており、概念的には同じように動作します。



2.1処理の準備



前述のように、例外の発生時に、プロセッサは対応するゲート記述子から関数を呼び出します。これは、割り込み記述子テーブルの要素です。 ゼロ除算ゲートウェイ機能は、KiDivideErrorFaultカーネル機能です。 以下の図11に、関数のアセンブラー表現を示します。 簡潔にするため、議論中のトピックに直接関連するコードの部分のみが表示されます。





図11



図からわかるように、最初に関数は空のエラーコードをシミュレートします。 ゼロによる除算を回避するために、プロセッサはコードをスタックにプッシュしません。 次に、関数は汎用レジスタをプッシュし、スタックにメモリを割り当て、フレームポインターを設定します。 これにより、関数のプロローグが終了します。 永続的な汎用レジスターとXMMレジスターは、関数の本体に格納されます。 この関数はまた、呼び出されたハンドラーのタイプをスタック変数に保存します。 値1は例外に対して常に設定され、0は割り込みに対して、2はサービスに対して設定されます。 ユーザー空間からカーネルサービスを呼び出します。 関数の最後のアクションは、KiExceptionDispatch関数を呼び出すことです。 呼び出す前に、関数は方向フラグをリセットし、XMMブロックのMXCSRレジスタを保存してから、標準値をロードします。 これについては、以下で詳しく説明します。 関数にはエピローグがないことに注意してください。 事実は、例外の処理後、ストリームの作業が通常の方法で再開されないことです。 KiExceptionDispatchは制御を返さないため、エピローグは必要ありません。 関数呼び出し命令の後、アイドル命令が続きます。 これは、いわゆるプレースホルダーです。 特別な役割が割り当てられており、その存在により、RtlVirtualUnwind関数は、関数本体の実行中に例外が発生したことを確実に判断できます。 つまり、そのようなプレースホルダーが存在しない場合、RlVirtualUnwind関数は、KiExceptionDispatch関数をプロモートするときに、 nop命令ではなくretn命令の戻りアドレスを抽出します。 したがって、プロモーションの次の反復(つまり、KiDivideErrorFault関数が既にアンワインドされている場合)で、RtlVirtualUnwind関数は、プロローグ、エピローグ、または本文が実行されたかどうかを分析します。 この記事の最初の部分のセクション1で既に述べたように、エピローグが実行されたかどうかは、関数自体のコードによって(またはUWOP_EPILOGタイプ、構造UNWIND_INFOバージョン2のレコードを使用して決定されます。 (コードバイトのストリームではなく命令アドレス)、およびretn命令はエピローグでのみ使用されるため、RtlVirtualUnwind関数は、本文ではなくエピローグが実行されたという誤った仮定を行います。 したがって、これにより、KiDivideErrorFault関数が巻き戻されたときに、プロローグが解かれず、関数の上のスタックに続くフレームのアドレスが誤って決定されるという事実につながります。



XMMブロックのMXCSRレジスターは、この記事の最初の部分のセクション3には記載されていませんが、関数を呼び出すときの呼び出し規則もその使用法を管理します。 このレジスタは、図12に示すように、定数部分と非定数部分に分けられます。





図12



不安定な部分は、ビット0〜5の6つのステータスフラグで構成されます。 6〜15の制御ビットで構成されるレジスタの残りの部分は、定数と見なされます。 呼び出された関数が定数部分の状態を変更した場合、戻る前にそれを復元する必要があります。 さらに、呼び出し元の関数は、他の関数を呼び出す前に、定数によって変更された場合に定数値を定数部分にロードする必要があります。 定数部分のフィールドの標準値:





これらのルールは、次の2つの場合にのみ違反できます。



  1. 関数の目的がレジスタの定数部分を変更することである場合;

  2. これらの規則の違反がプログラムの動作の変更につながらない場合、すなわち プログラムは、ルールに違反していないかのように動作します。


非定数部分の状態は、関数の境界で解釈されるべきではありません。 呼び出された関数はその値に依存するべきではありませんが、関数の説明で明示的に示されていない限り、呼び出し元の関数はそれに制御を返した後に呼び出します。



方向フラグ(DF)の場合、デフォルト値は0です。フラグが設定されている場合、関数を呼び出す前または関数から戻る前にフラグをリセットする必要があります。



KiExceptionDispatchは8つのパラメーターを取ります。 ECXには例外コードが含まれています。 この例外に固有のEDXパラメーターの数。 R8には、例外を発生させた命令のアドレスが含まれています。 レジスタR9、R10、R11には、この例外に固有のパラメーター値が含まれています。 RBPとRSPは、格納されている揮発性レジスタへのポインターです。 前述のように、関数は制御を返しません。 以下の図13に、関数のアセンブラー表現を示します。 簡潔にするために、議論中のトピックに直接関連するコードのセクションのみをリストします。





図13



図からわかるように、関数のプロローグは最初にスタックにメモリを割り当て、その後、定数XMMレジスタと汎用レジスタが保存されます。 これにより、関数のプロローグが終了します。 次に、この関数は、スタックに割り当てられたメモリ内のEXCEPTION_RECORD構造体を初期化し、KiDispatchException関数を呼び出します。 関数から戻った後、次が復元されます:汎用パーマネントレジスタ、定数XMMレジスタ、MXCSRレジスタ、非定数汎用レジスタ、非定数XMMレジスタ。 次に、ゲートウェイ関数によって割り当てられたスタック上のメモリ(この例では、KiDivideErrorFault関数によって割り当てられたメモリ)が解放され、中断されたストリームに戻ります。 EXCEPTION_RECORDの構造は、図14で定義されています。





図14



ExceptionCodeフィールドには、例外コードが含まれています。 ExceptionFlagsフィールドは、例外処理のタイプと状態を記述するビットフィールドです。 そのフラグは、ハンドラーの検索と呼び出しの議論の中で、またスタックの昇格の議論の中で詳細に調べられます。 場合によっては、ExceptionRecordフィールドには、同じタイプの別の構造体へのポインターが含まれます。 たとえば、例外ハンドラーまたはプロモーションハンドラーの検索中に無効な状況が見つかった場合(たとえば、ハンドラーが誤った処理結果を返した場合)、EXCEPTION_RECORD構造体にこの状況が発生した例外のEXCEPTION_RECORD構造体へのポインターが含まれる新しい例外がスローされます。 それ以外の場合、フィールドはNULLです。 このステートメントは32ビットバージョンのWindowsに当てはまり、64ビットバージョンではほとんど常にNULLであることに注意してください。 ExceptionAddressフィールドには、例外を発生させた命令のアドレスが含まれています。 NumberParametersフィールドには、特定のタイプの例外に固有のExceptionInformation配列内のパラメーターの数が含まれ、EXCEPTION_MAXIMUM_PARAMETERS定義は15、つまり これは、すべてのタイプの例外のパラメーターの最大数です。



KiDispatchException関数は5つのパラメーターを取ります。ExceptionRecord-例外の理由を説明するEXCEPTION_RECORD構造体へのポインター。 NonvolatileRegisters-定数レジスターへのポインター。 VolatileRegisters-揮発性レジスターへのポインター。 PreviousMode-例外が発生したスレッドのコンテキスト(ユーザーまたはカーネルコンテキスト)。 FirstChance-最初の処理の試行(TRUEまたはFALSE)。 関数は値を返しません。



ExceptionRecordは、例外の原因を説明します。 VolatileRegistersは、ゲートウェイ関数(この例ではKiDivideErrorFault関数)によって生成されます。 NonvolatileRegistersは、KiExceptionDispatch関数によって生成されます。 また、両方の構造には、例外発生時のレジスタの値だけでなく、この記事では説明しないその他の情報も含まれていることに注意してください。 議論中のトピックに直接関連するものではありません。 PreviousModeは、例外が発生したコンテキストに関する情報を保持し、KernelModeまたはUserModeに等しくなります。 FirstChanceは、この例外処理の試行が最初かどうかを示すブール値です。



KiDispatchException関数は、可能であれば、例外ハンドラー自体を関与させずに例外を処理します。 また、ユーザー空間で例外が発生した場合、例外処理はそれにリダイレクトされます。 関数の簡略ブロック図を図15に示します。





図15



図に示すように、作業の開始時に、関数はNonvolatileRegistersおよびVolatileRegistersポインターの構造からCONTEXT構造を形成し、これらの構造に含まれないプロセッサーレジスタを反映するフィールド(セグメントレジスタなど)も標準値で初期化されます。 したがって、この構造には、例外発生時のプロセッサレジスタの値が反映されます。



次に、関数はKiPreprocessFault関数を使用して、例外ハンドラーを使用せずに例外を処理しようとします。 例外が処理されなかった場合、カーネルコンテキストで発生した場合、関数はRtlDispatchException関数を呼び出します。この関数は、検索とハンドラーの呼び出しを実行します。



RtlDispatchException関数の処理が完了した後、およびCONTEXT構造体のフィールドは例外ハンドラーによって変更される可能性があるため、この構造体のフィールドは、KeContextToKframes関数によってNonvolatileRegistersおよびVolatileRegistersポインターを使用して構造体にコピーバックされ、それにより、中断されたストリームのコンテキストが変更されます。



ユーザーコンテキストで例外が発生した場合、セキュリティ上の理由で関数は関数ハンドラーによって呼び出されず、代わりに関数は例外時のRSPおよびRIP値をユーザースタックにコピーし、EXCEPTION_RECORDおよびCONTEXT構造体をユーザースタックにコピーして、カーネルマシンフレームを変更します関数から戻るとき、制御はユーザーモードハンドラーに転送されました。



ユーザーモードハンドラーへのポインターは、システムの初期化時に登録されます。 ユーザーコンテキストで例外を処理する機能は、KiUserExceptionDispatchと呼ばれるntdll.dllライブラリにあります。 ユーザー空間に対してカスタム例外ハンドラーが呼び出されるという事実にもかかわらず、それはカーネルモードハンドラーに非常に似ているため、その操作についてこれ以上説明する必要はありません。



2.2ハンドラーを検索して呼び出す



前述したように、RtlDispatchException関数はハンドラーを検索して呼び出します。 この関数は2つのパラメーターを取ります。ExceptionRecord-例外の原因を説明するEXCEPTION_RECORD構造体へのポインター。 ContextRecordは、例外発生時のプロセッサレジスタの状態を記述するCONTEXT構造体へのポインタです。 この関数は、ブール値、例外が処理された場合はTRUE、そうでない場合はFALSEを返します。



RtlDispatchException関数は、呼び出された関数のスタックで順次スキャンを実行します。 関数にハンドラーがある場合、RtlDispatchException関数はそれを呼び出します。 ハンドラーがExceptionContinueExecutionを返す場合、RtlDispatchException関数は動作を停止します。それ以外の場合、ハンドラーの検索は続行します。 以下の図16に、機能のブロック図を示します。





図16



作業の開始時に、関数はスタックの下限と上限を受け取ります。 例外ハンドラーが呼び出されると、例外時のプロセッサーの状態を記述する構造体へのポインターが渡されるため、関数は検索中に仮想スタックの巻き戻しを実行するため、送信されたCONTEXT構造体の内容が変更され、関数はその内容をローカル変数にコピーします。



次に、関数はEXCEPTION_RECORD構造体のExceptionFlagsフィールドの初期値を形成します。 送信された構造体のフィールドには、EXCEPTION_NONCONTINUABLEフラグのセットが含まれている場合があります。これは、中断されたストリームの継続が不可能であることを示します。 したがって、初期値を初期化するときに、関数はこのフラグを渡された構造体からローカル変数にコピーします。 次に、関数は関数のフレームポインターを無効にし、その例外ハンドラーは、その実行中に例外(つまり、ネストされた例外)を発生させ、渡されたEXCEPTION_RECORD構造体から例外を生成した命令のアドレスをローカル変数にコピーします。



次に、関数はRtlLookupFunctionEntry関数を使用して、PEイメージのアドレスと、例外が発生したこのイメージのRUNTIME_FUNCTION関数構造体へのポインターを受け取ります。 関数がポインターを返さなかった場合、単純な関数の実行中に例外が発生したと見なされます。これは、前述のように、プロモーション情報がありません。 なぜなら 単純関数はスタックにメモリを割り当てないため、RSP値は戻りアドレスを示します。したがって、そのような関数の場合、RtlDispatchException関数はこのアドレスを抽出し、その値をローカルCONTEXT構造のRipフィールドにコピーし、同じ構造のRspフィールドの値を8増やします単純な関数のプロモーションをシミュレートします。 これで、ローカルCONTEXT構造体の内容は、上のスタックの次の関数の実行状態を記述します。 次に、ローカルCONTEXT構造の関数は、上のスタックの次の関数に属する命令のアドレスをローカル変数にコピーし、RtlpIsFrameInBounds関数を使用して、新しいRSPポインターがスタック制限内にあることを確認します。 ポインターがこれらの制限を超えた場合、例外ハンドラーが見つからなかったことを意味するため、RtlDispatchException関数はFALSEを返します。 それ以外の場合、関数は、新しいスタックの次の関数の新しい命令のアドレスに対して、PEイメージのアドレスとRUNTIME_FUNCTION構造体へのポインターの受信から始めて、作業を続行します。



人事機能の場合、RtlLookupFunctionEntry関数はRUNTIME_FUNCTION構造体へのポインターを返します。 この場合、このような関数の昇格は、RtlVirtualUnwind関数を使用して実行されます。RtlVirtualUnwind関数は、ツイストされていない関数のフレームポインターを返します。 プロモーションの直後に、フレームポインターがスタック制限内にあるかどうかのチェックが行われます。 フレームポインターがこれらの制限を超えた場合、RtlDispatchException関数は、渡されたCONTEXT構造体のExceptionFlagsフィールドにEXCEPTION_STACK_INVALIDフラグを設定し、FALSEを返します。それ以外の場合、RtlVirtualUnwind関数がツイストされていない関数の例外ハンドラーへのポインターを返さなかった場合、RtlDispatchException関数は、最初にこの関数に属する命令のアドレスをコピーし、ローカルCONTEXT構造体のRspフィールドの値をチェックして制限を超えることにより、上のスタックの次の関数を巻き戻しますスタック。



RtlVirtualUnwind関数が例外ハンドラーへのポインターを返した場合、RtlDispatchException関数はそれを呼び出します。呼び出す前に、関数は、ローカルコピーから渡されたEXCEPTION_RECORD構造体のExceptionFlagsフィールドの内容を更新します。例外ハンドラーは、この記事の第2部のセクション3で最初に説明され、プロトタイプは図5に示されています。ハンドラーを呼び出す前に、関数はDISPATCHER_CONTEXT構造体を準備します。構造の定義を図17に示します。





図17



ControlPcフィールドには、ハンドラーが呼び出された関数の本体に属するアドレスが含まれています。 ImageBaseフィールドには、関数とそのハンドラーを含むPEイメージの先頭のアドレスが含まれています。 FunctionEntryフィールドには、同じ関数の構造体のアドレスRUNTIME_FUNCTIONが含まれています。 EstablisherFrameフィールドには、関数フレームポインターが含まれています。 TargetIpフィールドはプロモーションに使用され、その議論の中で詳細に議論されます。 ContextRecordフィールドには、ハンドラー検索の現在の状態を反映するCONTEXT構造体へのポインターが含まれます。 RtlDispatchException関数のローカル変数へのポインター。 LanguageHandlerフィールドには、呼び出されたハンドラーのアドレスが含まれます。 HandlerDataフィールドには、対応するプログラミング言語に固有のデータのアドレスが含まれています。 HistoryTableフィールドには、検索キャッシュテーブルへのポインターが含まれています。ScopeIndexフィールドには、RtlDispatchException関数のローカル変数の現在の値が含まれます。その目的については、プロモーションの説明で詳しく説明します。 Fill0フィールドはまったく使用されず、位置合わせのために存在します。



RtlDispatchException関数は、例外ハンドラーを直接呼び出さず、代わりにRtlpExecuteHandlerForExceptionヘルパー関数を使用します。この関数は、ハンドラーと同じパラメーターを受け取り、同じ値を返します。この関数は、実際には例外ハンドラーの関数のラッパーであり、例外ハンドラー自体の実行中に発生した例外をキャッチするために使用されます。関数のアセンブラー表現を図18に示します。





図18



図に示すように、まず関数はレジスタ変数と1つの変数にスタック上のメモリを割り当て、この変数にDISPATCHER_CONTEXTに渡された構造体へのポインターを格納し、アドレスがDISPATCHER_CONTEXT構造体のLanguageHandlerフィールドに格納されている例外ハンドラーを呼び出します。また、プレースホルダー関数本体の存在にも注意してください。前述の必要性の理由に加えて、もう1つ追加されます:例外ハンドラーは関数の本体にバインドされているため、プレースホルダーがない場合は呼び出されず、したがって、この理由でRtlDispatchException関数の操作がさらに違反されます。例外ハンドラー関数のアセンブラー表現を図19に示します。





図19



図に示すように、ハンドラーは最初にプロモーションが実行されているかどうかを確認し、実行されている場合、関数はExceptionContinueSearchを返し、それによりプロモーション関数に検索エンジンの継続の指示を与えます。それ以外の場合、例外ハンドラーが検索され、その間に別の例外が発生し、ハンドラーが新しい例外を生成した関数のフレームポインターを現在のハンドラー検索プロセスのDISPATCHER_CONTEXT構造にコピーする必要がありました。



DISPATCHER_CONTEXT構造体が準備された後、RtlDispatchException関数は例外ハンドラーをスローします。ハンドラーによって渡されたEXCEPTION_RECORD構造体に設定されている場合、ハンドラーを呼び出した直後に、関数はフラグのローカルコピーにEXCEPTION_NONCONTINUABLEフラグを設定します。次に、関数はローカルコピーのEXCEPTION_NESTED_CALLフラグをリセットし、関数のフレームポインターをリセットします。このハンドラーの実行中に、この関数のフレームポインターが以前に修正されたものと一致する場合、例外が発生します。以下は、結果に応じた関数の対応するアクションを説明しています。



ハンドラーがExceptionContinueSearchを返した場合、関数は最初にこの関数に属するアドレスをコピーし、スタックの制限を超えるようにローカルCONTEXT構造体のRspフィールドの値をチェックすることにより、上記のスタックの次の関数を昇格させます。



ハンドラーがExceptionContinueExecutionを返した場合、関数はすぐに操作を停止し、TRUEを返します。以前は、関数はEXCEPTION_NONCONTINUABLEフラグが設定されていないことを確認します。そうでない場合、関数はSTATUS_NONCONTINUABLE_EXCEPTION例外をスローします。



ハンドラがExceptionNestedExceptionをスローする場合、これは、新しい例外が発生したコンテキストで、検索プロセスで例外ハンドラの別の不完全な検索プロセスが見つかったことを意味します。この場合、DISPATCHER_CONTEXT構造体のEstablisherFrameフィールドには、例外ハンドラーが例外を発生させた関数のフレームポインターが含まれます。前述のように、この値はRtlpExecuteHandlerForException関数の例外ハンドラーをそこにコピーします。 RtlDispatchException関数は、ExceptionFlagsフィールドにEXCEPTION_NESTED_CALLフラグを設定し、ハンドラーが例外を発生させた関数のフレームポインターも更新します。この値は、現在のポインター値が0(ネストされた例外がなかった)、またはDISPATCHER_CONTEXT構造体のEstablisherFrameフィールドに関数フレームポインターが含まれている場合にのみ更新されます。これは、新しい例外が発生したコンテキスト内の関数よりも上のスタックにあります。



ハンドラーがExceptionCollidedUnwindを返した場合、これは、例外が発生したコンテキストで、検索プロセスでアクティブなプロモーションが検出されたことを意味します。このケースは、スタックのプロモーションを説明する際に詳細に説明します。ここでは、この結果に応じて、RtlDispatchException関数がDISPATCHER_CONTEXT構造体とローカルCONTEXT構造体を更新し、プロモーションの中断された場所からハンドラーの検索が再開されることを示す必要があります。



他のすべての場合、RtlDispatchException関数はSTATUS_INVALID_DISPOSITION例外をスローします。



おわりに



セクション2で既に述べたように、プロセス全体を条件付きで2つの部分に分けることができ、最初の部分を完全に検討しました。次のセクションでは、 finallyブロック/除き、またしてみてください/スタックの推進や動作原理試みを含む第2の部分を、考慮されます。



All Articles