共通言語ランタイム (CLR: Common Language Runtime) のデバッグ API は、オペレーティング システム カーネルの一部のような感覚で使用できるように設計されています。 アンマネージ コードの場合、プログラムで例外が発生すると、カーネルがプロセスの実行を中断し、Win32 デバッグ API を使用してデバッガーに例外情報を渡します。 マネージ コードに対しては、CLR デバッグ API によって同じ機能が提供されます。 マネージ コードで例外が発生すると、CLR デバッグ API がプロセスの実行を中断し、デバッガーに例外情報を渡します。
ここでは、CLR デバッグ API がどの時点でどのように使用されるかと、この API に用意されているサービスについて説明します。
プロセスのアーキテクチャ
CLR デバッグ API には、次の 2 つの主要コンポーネントが組み込まれています。
デバッグ DLL。常にデバッグ対象プログラムと同じプロセスに読み込まれます。 CLR との通信、およびマネージ コードを実行しているスレッドの実行制御と検査は、ランタイム コントローラーが行います。
デバッガー インターフェイス。デバッグ対象プログラムとは別のプロセスに読み込まれます。 デバッガー インターフェイスは、デバッガーの代わりにランタイム コントローラーとの通信を行います。 また、デバッグ中のプロセスから発生した Win32 デバッグ イベントを受け取り、それらを処理するか、またはアンマネージ コードのデバッガーに渡す役割も果たします。 デバッガー インターフェイスは、公開された API を持つ CLR デバッグ API の一部にすぎません。
CLR デバッグ API は、コンピューター間またはプロセス間のリモート使用をサポートしていません。つまり、この API を使用するデバッガーは、次の API アーキテクチャの図に示すように、独自のプロセス内からこの API を使用する必要があります。 この図は、CLR デバッグ API の各種コンポーネントの場所と、それらのコンポーネントが CLR およびデバッガーとやり取りする方法を示しています。
CLR デバッグ API のアーキテクチャ
マネージ コードのデバッガー
マネージ コードのみをサポートするデバッガーをビルドすることもできます。 CLR デバッグ API では、ソフトアタッチ機構を使用して、必要に応じてこのようなデバッガーをプロセスにアタッチできます。 プロセスにソフトアタッチされたデバッガーは、後でプロセスからデタッチできます。
スレッドの同期
CLR デバッグ API には、プロセスのアーキテクチャに関連する矛盾した要件があります。 デバッグ ロジックをデバッグ対象プログラムと同じプロセスに読み込むことには、多くの説得力のある理由があります。 たとえば、データ構造が複雑で、固定的なメモリ レイアウトではなく関数によって操作することが多い場合、 データ構造をプロセスの外部からデコードするよりも、それらの関数を直接呼び出す方がはるかに簡単です。 デバッグ ロジックを同じプロセスに読み込むもう 1 つの理由は、プロセス間通信のオーバーヘッドをなくし、パフォーマンスを向上させるためです。 さらに、CLR デバッグの重要な機能として、ユーザー コードをプロセス内でデバッグ対象と共に実行できることがありますが、それにはデバッグ対象プロセスとの連携が必要になります。
一方、CLR のデバッグはアンマネージ コードのデバッグと共存させる必要がありますが、これは外部プロセスからでなければ正常に実行できません。 また、アウトプロセス デバッガーでは、デバッガーの操作とデバッグ対象プロセスとの干渉が最小限に抑えられるため、アウトプロセス デバッガーの方がインプロセス デバッガーより安全です。
このような相反する要件が存在するため、CLR デバッグ API は、それぞれの方法を組み合わせています。 主要なデバッグ インターフェイスはアウトプロセスであり、ネイティブ Win32 デバッグ サービスと共存します。 ただし、CLR デバッグ API では、デバッグ プロセスとの同期機能が追加され、ユーザー コードを安全に実行できるようになっています。 この同期を実行するために、API はオペレーティング システムおよび CLR と連携して、処理の中断が生じない位置でプロセス内のすべてのスレッドを中断し、ランタイムを不統一な状態のままにしておきます。 このとき、デバッガーは、特殊なスレッドでコードを実行して、ランタイムの状態を確認したり、必要に応じてユーザー コードを呼び出したりできます。
マネージ コードがブレークポイント命令を実行するか、例外を生成すると、ランタイム コントローラーに通知されます。 このコンポーネントは、マネージ コードを実行しているスレッドとアンマネージ コードを実行しているスレッドを特定します。 通常、マネージ コードを実行しているスレッドでは、安全に中断できる状態になるまで実行の継続が許容されます。 たとえば、そのスレッドで実行中のガベージ コレクションを終了しなければならない場合があります。 マネージ コードのスレッドが安全な状態になると、すべてのスレッドが中断されます。 次に、デバッガー インターフェイスからデバッガーに、ブレークポイントまたは例外の受信が通知されます。
アンマネージ コードがブレークポイント命令を実行するか、例外を生成すると、デバッガー インターフェイス コンポーネントが Win32 デバッグ API から通知を受け取ります。 この通知はアンマネージ デバッガーに渡されます。 マネージ コードのスタック フレームを検査できるようにする場合など、同期の実行が必要であるとデバッガーが判断した場合、デバッガー インターフェイスでは、停止したデバッグ対象プロセスを再起動し、ランタイム コントローラーに同期を実行するように通知する必要があります。 同期が完了すると、デバッガー インターフェイスに通知されます。 この同期は、アンマネージ デバッガーに対して透過的に行われます。
ブレークポイント命令または例外を生成したスレッドに対しては、同期中の実行を禁止する必要があります。 これを実現しやすくするために、デバッガー インターフェイスは、スレッドのフィルター チェーンに特殊な例外フィルターを設定してスレッドを制御します。 スレッドが再起動されると、そのスレッドは例外フィルターに入り、ランタイム コントローラーに制御されます。 例外処理を続行するとき (または例外を取り消すとき) には、フィルターはスレッドの通常の例外フィルター チェーンに制御を戻すか、または適切な結果を返して実行を再開します。
まれに、ネイティブ例外を生成するスレッドが重要なロックを保持していて、そのロックが解放されるまでランタイムの同期を完了できないことがあります。 通常、この現象は、malloc ヒープ上のロックなど、低水準ライブラリのロックによって発生します。 このような場合、同期操作はタイムアウトになり、同期は失敗します。 これにより、同期が必要な一定の操作も失敗します。
インプロセス ヘルパー スレッド
CLR デバッグ API の正常な動作を保証するために、すべての CLR プロセス内では、1 つのデバッガー ヘルパー スレッドが使用されます。 このヘルパー スレッドは、一定の状況下でのスレッド同期を補助すると共に、デバッグ API の多くの検査サービスを処理します。 ICorDebugProcess::GetHelperThreadID メソッドを使用すると、ヘルパー スレッドを識別できます。
JIT コンパイラとのやり取り
Just-In-Time (JIT) コンパイルされたコードをデバッガーでデバッグできるようにするために、CLR デバッグ API には、Microsoft Intermediate Language (MSIL) バージョンの関数の情報をネイティブ バージョンの関数に対応付ける機能があります。 この情報には、コード内のシーケンス ポイントやローカル変数の位置情報などが含まれます。 .NET Framework Version 1.0 および 1.1 では、これらの情報が生成されるのは、ランタイムがデバッグ モードの場合だけでした。 .NET Framework Version 2.0 では、この情報は常に生成されます。
JIT コンパイルされたコードの高度な最適化も可能です。 ただし、共通部分式の削除、関数のインライン展開、ループのアンワインド、コードのホイストなどの最適化によって、実行時に呼び出される関数の MSIL コードとネイティブ コードとの関連付けが失われる可能性があります。 つまり、このような積極的なコード最適化手法は、正しいマッピング情報を提供する JIT コンパイラの機能に重大な影響を及ぼします。 そのため、ランタイムをデバッグ モードで実行する場合には、JIT コンパイラは特定の最適化を行わなくなります。 この制限により、デバッガーは、ソース行のマッピングや、すべてのローカル変数および引数を正確に認識することができます。
デバッグ モード
CLR デバッグ API には、特殊なデバッグ モードが 2 つあります。
エディット コンティニュ モード。 このモードでは、コードを後から変更できるようにするために、ランタイムの動作が変わります。 これは、エディット コンティニュをサポートするには、特定のランタイム データ構造のレイアウトを変更する必要があるためです。 このモードはパフォーマンスが低下するため、エディット コンティニュ機能が必要な場合以外には使用しないでください。
デバッグ モード。 このモードでは、JIT コンパイラによる最適化が省略されます。 そのため、ネイティブ コードの実行と高水準言語のソースとの一致度が高くなります。 このモードもパフォーマンスが低下するため、必要な場合以外には使用しないでください。
エディット コンティニュ モード以外でプログラムをデバッグする場合、エディット コンティニュ機能はサポートされません。 デバッグ モード以外でプログラムをデバッグする場合、多くのデバッグ機能はサポートされますが、最適化によって動作が不正になる可能性があります。 たとえば、シングル ステップを実行するとメソッド内の行をランダムにジャンプしたり、インライン メソッドがスタック トレースに表示されなかったりすることがあります。
ランタイムが初期化される前にデバッガーがプロセスの制御を取得していれば、デバッガーで CLR デバッグ API を使用して、エディット コンティニュ モードとデバッグ モードをプログラムから有効にできます。 多くの用途ではこの方法で十分です。 ただし、既に実行を開始しているプロセス (JIT デバッグの途中のプロセスなど) にアタッチするデバッガーでは、これらのモードを開始することはできません。
このような問題に対処するには、プログラムをデバッガーとは別個に JIT モードまたはデバッグ モードで実行する方法があります。 デバッグを有効にする方法の詳細については、「デバッグ、トレース、およびプロファイリング」を参照してください。
JIT 最適化を行うと、アプリケーションのデバッグ機能は低下します。 CLR デバッグ API では、最適化された JIT コンパイル済みコードのスタック フレームやローカル変数を検査できます。 ステップ実行もサポートされていますが、正確さは保証されません。 そこで、JIT コンパイラによる JIT 最適化をすべてオフにするプログラムを実行すると、デバッグ可能なコードを生成できます。 詳細については、「イメージのデバッグの簡略化」を参照してください。