ここでは、共通言語ランタイム (CLR: Common Language Runtime) デバッグ API を使用したコードの動的な挿入について説明します。 動的コード挿入は、元の移植可能な実行可能 (PE: Portable Executable) ファイルに存在しない関数を実行します。 たとえば、動的コード挿入を使用すると、Microsoft Visual Studio の統合開発環境 (IDE: Integrated Development Environment) でデバッグしながら、[イミディエイト] ウィンドウで式を実行できます。 CLR は、アクティブなスレッドをハイジャックしてコードを実行します。 デバッガーが残りのスレッドの実行または停止を CLR に要求する場合があります。 動的コード挿入は関数の評価に基づいているので、動的に挿入された関数を通常のコードと同じようにデバッグできます。 つまり、ブレークポイントの設定やステップ実行などのすべての標準デバッグ サービスを、動的に挿入されたコードに対して呼び出すことができます。
コードを動的に挿入するためには、デバッガーで次の操作を行う必要があります。
動的なコードを実行する関数を作成します。
エディット コンティニュを使用してデバッグ対象にコードを挿入します。
作成した関数を呼び出して、コードを実行します。必要に応じて繰り返します。
以下では、これらの手順について詳しく説明します。
関数の作成
動的なコードを実行する関数のシグネチャを計算します。 そのためには、ローカル変数のシグネチャを、リーフ フレームで実行されているメソッドのシグネチャに追加します。 このようにして作成されるシグネチャでは、使用される変数の最小サブセットの計算は必要ありません。 ランタイムは、使用されていない変数を無視します。 詳細については、後の「メタデータからのシグネチャの取得」を参照してください。
関数のすべての引数は、ByRef と宣言されている必要があります。 これにより、関数の評価は、挿入された関数での変数の変更を、デバッグ対象のリーフ フレームに反映できます。
動的なコードの実行時に、一部の変数がスコープ内にない可能性があります。 その場合は、null 参照を渡す必要があります。 スコープ外の変数を参照すると、NullReferenceException がスローされます。 この場合、デバッガーは ICorDebugManagedCallback::EvalException コールバックを呼び出すことで、関数の評価を完了できます。
関数の一意の名前を選択します。 ユーザーが関数を参照するのをデバッガーで防止できるように、関数名の先頭には "_Hidden:" という文字列を付加する必要があります。 この関数を追加すると、関数名が特殊であることを示すフラグが設定されます。
コードの挿入
デバッガーは、動的に挿入されるコードを本体として含んでいる関数のビルドをコンパイラに依頼する必要があります。
デバッガーでデルタ PE ファイルを計算します。 これを行うには、デバッガーで ICorDebugModule::GetEditAndContinueSnapshot メソッドを呼び出して ICorDebugEditAndContinueSnapshot オブジェクトを取得します。 デバッガーは、ICorDebugEditAndContinueSnapshot メソッドを呼び出してデルタ PE イメージを作成し、ICorDebugController::CommitChanges を呼び出して実行中のイメージにデルタ PE をインストールします。
動的に挿入される関数は、実行するリーフ フレームと同じレベルの参照範囲に配置される必要があります。 リーフ フレームがインスタンス メソッドである場合は、動的に挿入される関数も同じクラスのインスタンス メソッドでなければなりません。 リーフ フレームが静的メソッドである場合は、動的に挿入される関数も静的メソッドでなければなりません。 グローバル メソッドは、特定のクラスに属する静的メソッドです。
メモ
関数は、動的コード挿入プロセスが完了した後でも、デバッグ対象内に存在します。これにより、関数を作成しなおしてコードを再び挿入しなくても、前に挿入したコードを繰り返し再評価できます。したがって、以前に挿入したコードについては、このセクションと前のセクションで説明した手順を省略できます。
挿入されたコードの実行
デバッグ検査ルーチンを使用して、シグネチャの各変数の値 (ICorDebugValue オブジェクト) を取得します。 ICorDebugThread::GetActiveFrame メソッドまたは ICorDebugChain::GetActiveFrame メソッドを使用して、リーフ フレーム、および ICorDebugILFrame に対する QueryInterface を取得できます。 ICorDebugILFrame::EnumerateLocalVariables、ICorDebugILFrame::EnumerateArguments、ICorDebugILFrame::GetLocalVariable、または ICorDebugILFrame::GetArgument のいずれかのメソッドを呼び出して、実際の変数を取得します。
メモ
CORDBG_ENABLE が設定されていないデバッグ対象 (つまり、デバッグ情報を収集していないデバッグ対象) にデバッガーがアタッチされていると、デバッガーは ICorDebugILFrame オブジェクトを取得できないため、関数の評価のための値も収集できなくなります。
動的に挿入される関数の引数のオブジェクトに対する逆参照が弱いか強いかは、重要ではありません。 関数を評価するとき、ランタイムは挿入が行われたスレッドをハイジャックします。 これにより、元のリーフ フレームと元の厳密な参照はすべてスタック上に残ります。 ただし、デバッグ対象の参照がすべて弱い場合は、動的コード挿入を実行すると、ガベージ コレクションが行われ、オブジェクトがガベージ コレクトされる可能性があります。
ICorDebugThread::CreateEval メソッドを使用して ICorDebugEval オブジェクトを作成します。 ICorDebugEval には、関数を評価するメソッドが用意されています。 これらのメソッドのいずれかを呼び出します。 ICorDebugEval::CallFunction などのメソッドは、関数の評価の設定だけを行います。 デバッガーは、ICorDebugController::Continue を呼び出し、デバッグ対象を実行して関数を評価する必要があります。 評価が完了すると、デバッグ サービスは ICorDebugManagedCallback::EvalComplete メソッドまたは ICorDebugManagedCallback::EvalException メソッドを呼び出して、関数の評価をデバッガーに通知します。
関数の評価からオブジェクトが返る場合、そのオブジェクトは厳密に参照されます。
動的に挿入されたコードが、コードをラップする関数に渡された null 参照を逆参照しようとすると、CLR のデバッグ サービスは ICorDebugManagedCallback::EvalException を呼び出します。 これに対して、デバッガーは挿入されたコードを評価できないことをユーザーに通知する場合があります。
ICorDebugEval::CallFunction メソッドは仮想ディスパッチを行いません。仮想ディスパッチを行う必要がある場合は、代わりに ICorDebugObjectValue::GetVirtualMethod を使用してください。
デバッグ対象がマルチスレッドであり、他のスレッドが実行しているとデバッガーにとって好ましくない場合は、デバッガーは ICorDebugController::SetAllThreadsDebugState を呼び出して、関数の評価に使用するスレッドを除くすべてのスレッドの状態を、THREAD_SUSPEND に設定する必要があります。 動的に挿入されるコードの処理によっては、この設定によりデッドロックが発生する可能性があります。
その他の問題
.NET Framework のセキュリティはコンテキスト ポリシーによって決まるので、セキュリティ設定を明示的に変更しない限り、動的コード挿入はリーフ フレームと同じセキュリティ アクセス許可および機能で動作します。
エディット コンティニュ機能で関数を追加できる場所であればどこでも、動的に挿入される関数を追加できます。 論理的に選択するなら、リーフ フレームに追加します。
エディット コンティニュ操作を使用してクラスに追加できるフィールド、インスタンス メソッド、または静的メソッドの数には、制限はありません。 許容される静的データの最大量は定義されており、現在はモジュールごとに 1 MB に制限されています。
ローカルでない goto ステートメントは、動的に挿入されるコードでは使用できません。
メタデータからのシグネチャの取得
メタデータ ディスペンサーの取得
デバッガーは、REFIID IID_IMetaDataDispenser を使用して ICorDebugModule::GetMetaDataInterface メソッドを呼び出して、メタデータ ディスペンサーを取得します。
デバッガーは、REFIID IID_IMetaDataImport を使用して IMetaDataDispenser::OpenScope メソッドを呼び出して、IMetaDataImport インターフェイスを取得します。
IMetaDataImport を用いたメソッドの検索
デバッガーは、IMetaDataImport::GetMethodProps を呼び出し、トークンを使用してメソッドを検索します。 このメソッド トークンは、ICorDebugFunction::GetToken メソッドを使用して取得できます。 IMetaDataImport::GetMethodProps は、メソッドのシグネチャを返します。
デバッガーは、IMetaDataImport::GetSigFromToken メソッドを呼び出して、ローカル シグネチャ、つまりローカル変数のシグネチャを取得します。 デバッガーは、ローカル変数のシグネチャのトークンを指定する必要があります。 このトークンは、ICorDebugFunction::GetLocalVarSigToken メソッドを呼び出すことで取得できます。
関数のシグネチャの作成
シグネチャの形式は「Type and Signature Encoding in Metadata」の仕様に記述されており、この概要の情報より優先されます。
仕様の「Method Declaration」セクションには、メソッドのシグネチャの形式に関する説明があります。 この形式は、呼び出し規約を示す 1 バイトの後に引数の数を示す 1 バイトが続き、さらに型のリストが続きます。 各型のサイズは異なっていてもかまいません。 可変引数呼び出し規約を指定する場合、引数の数は引数の総数、つまり固定引数と可変引数を加えた数になります。 ELEMENT_TYPE_SENTINEL バイトは、固定引数が終了して可変引数が開始する位置を示します。
仕様の「Stand-Alone Signatures」セクションには、ローカル シグネチャの形式に関する説明があります。 スタンドアロン シグネチャは、可変引数の呼び出し規約を使用しません。
メソッドのシグネチャとローカル シグネチャを取得した後、デバッガーは新しいシグネチャのための領域を割り当てる必要があります。 その後、デバッガーはメソッドのシグネチャを反復処理します。 新しいシグネチャには、それぞれの型について、ELEMENT_TYPE_BYREF バイトと型を格納する必要があります。 メソッド終了のシグネチャまたは ELEMENT_TYPE_SENTINEL が指定されている型に達するまで、このプロセスを繰り返します。 次に、デバッガーは、ローカル シグネチャの型をコピーし、各型に ELEMENT_TYPE_BYREF を指定する必要があります。 メソッドのシグネチャに可変引数呼び出し規約がある場合は、これらの型をコピーして、ELEMENT_TYPE_BYREF を指定します。 最後に、デバッガーは引数の数を更新する必要があります。