更新 : 2007 年 11 月
共通言語ランタイム (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 が 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 最適化をすべてオフにするプログラムを実行すると、デバッグ可能なコードを生成できます。詳細については、「イメージのデバッグの簡略化」を参照してください。