Java Platform Debugger Architecture
概要
Java Platform Debugger Architecture (JPDA)は、2つのインタフェース(JVM TIとJDI)、1つのプロトコル(JDWP)、およびそれらのコンポーネントを結び合わせる2つのソフトウェア・コンポーネント(バックエンドとフロントエンド)で構成されています。 JVM TIの目的は多面的です。
- Javaプログラミング言語のデバッグ・ツールを、ハードウェア、オペレーティング・システム、仮想マシンの実装など、プラットフォーム固有の詳細事項に注意を払わずに簡単に作成できるようにする、標準的なインタフェース(説明を簡潔にするため、JDWPもインタフェースと呼ぶ)を提供する。
- これらのインタフェースを実装する完全なアーキテクチャ(リモート・デバッグおよびクロスプラットフォーム・デバッグを含む)を記述する。
- このアーキテクチャのリファレンス実装を提供する。
- インタフェースの実装またはクライアント(あるいはその両方)がリファレンス実装と違っていたり、JPDAコンポーネントと違っていたりしても構わない、高度にモジュール化されたアーキテクチャを提供する。
背景
「Java Platform Debugger Architecture」を参照してください。
モジュール性
ここでは、JPDAのモジュール化された構造の詳細を説明します。 それぞれの説明では、標準的なJPDAの使用法を取り上げます。 また、リファレンス実装について説明し、インタフェースの代替実装およびクライアントについても説明します。
JVM TIのモジュール性
Java Virtual Machine Tool Interface (JVM TI)では、仮想マシン(VM)で動作するJavaプログラミング言語のアプリケーションをデバッグできるようにするため、そのVMから提供される機能について記述されています。 JPDAでは、JVM TIはVMによって実装され、クライアントはJPDAのバックエンドになります。 JPDAのリファレンス実装では、JVM TIはJava HotSpot VMによって実装され、クライアントはバックエンドのリファレンス実装(JDKに付属の、jdwp.soやjdwp.dllなどのネイティブ共用ライブラリとして提供される)になります。
Java HotSpot VM以外の多くのVMも、JVM TIを実装しています。 バックエンドのリファレンス実装は、他のいくつかのプラットフォームに移植されました。 さらに、バックエンド以外にもJVM TIのクライアントがあります。もっとも有名なのは、ネイティブ・コードとJavaプログラミング言語コードの両方のデバッグを可能にするアプリケーション・エージェント(ネイティブ・レベルの制御と情報を必要とする)です。 バックエンドのクリーンルーム実装を意識する必要はありません。そのようなバックエンドを作成することも可能ですが、相当な労力を必要とします。
JDWPのモジュール性
Java Debug Wire Protocol (JDWP)は、デバッグ対象とデバッガの間でやり取りされるデバッグ情報および要求の形式を記述しています。 JPDAでは、フロントエンド(デバッガのプロセス内にある)とバックエンド(デバッグ対象のプロセス内にある)との間に通信チャネルがあります。そのチャネル上を流れるデータの形式が、JDWPによって記述されています。 JPDAのリファレンス実装では、バックエンドのリファレンス実装(前述)がこのチャネルのデバッグ対象側を提供し、フロントエンドのリファレンス実装(tools.jarにあるJDKのJavaプログラミング言語コンポーネント)がこのチャネルのデバッガ側を提供します。
一部のVMでは、JVM TIの実装に問題があります。 そのようなVMでは、JDWPが直接実装されます。 クライアント側では、Javaプログラミング言語で作成されていないアプリケーションは、JDIを使用するアプリケーションとして不適格なことがあります。 アプリケーションによっては、JDWPのクライアントとして実装することもあります。
JDIのモジュール性
Java Debug Interface (JDI)は、Javaプログラミング言語のアプリケーションをデバッグするためのpure Javaプログラミング言語インタフェースを提供します。 JPDAでは、JDIは、デバッガのプロセスから見たdebuggeeプロセスの仮想マシンのリモート・ビューです。 これは、フロントエンド(前述)によって実装されますが、そのクライアントになるのはデバッガ役のアプリケーション(IDE、デバッガ、トレース・ツール、モニター・ツールなど)です。
JDIは、アプリケーションの静的なビューを提供するようにシステムによって実装されることがあります。 また、JDWPのフロントエンドとはまったく違うメカニズムで情報を収集したりVMを制御したりするように実装されることもあります。
動作の概要
これまでは、各インタフェースを使用する様々な方法について説明してきました。 このセクションでは、標準的なJPDAが全体としてどのように動作するかを見ていきます。 説明の中では、個々の呼出しやコードの詳細について、実例を取り上げます。 そのような実例は、理解できなくても問題ありません。実例を具体的にするために紹介してあるだけです。
各インタフェースを橋渡しするのは、要求とイベントという2つのアクティビティです。 要求は、デバッガ側から出されるもので、情報の照会、リモート側のVMやアプリケーションの状態変更の設定、およびデバッグ状態の設定が含まれています。 イベントは、debuggee側から出されるもので、リモート側のVMやアプリケーションの状態変化を示しています。
1つの実例を調べてみましょう。 IDEのスタック表示でユーザーがローカル変数をクリックし、その値を要求したとします。 IDEは、JDIを使用してその値を取得します。具体的には、getValue
メソッドを呼び出します。たとえば:
theStackFrame.getValue(theLocalVariable)
ここで、theStackFrame
はcom.sun.jdi.StackFrame
であり、theLocalVariable
はcom.sun.jdi.LocalVariable
です。
次に、フロントエンドは、この要求を通信チャネル(たとえば、ソケット)経由で、debuggeeプロセスが動作しているバックエンドに送ります。 そのとき、フロント・エンドは、その要求をJDWPに準拠したバイト・ストリームの形式に変換します。 具体的に言うと、フロント・エンドはGetValuesコマンド(バイト値1)をStackFrameコマンド・セット(バイト値16)で送り、そのあとにスレッドID、フレームIDなどが続きます。
バックエンドは、そのバイト・ストリームを解析し、JVM TIを介してVMに照会を送ります。 具体的には、要求された値が整数だとすると、次のようなJVM TI関数の呼出しを実行します。
error = jvmti->GetLocalInt(frame, slot, &intValue);
バックエンドは、ソケット経由で応答パケットを返送します。そのパケットにはintValue
の値が入っており、JDWPに準拠したデータ形式にフォーマットされます。 フロントエンドは、応答パケットを解析し、その値をgetValue
メソッド呼出しの値として返します。 最後に、IDEは、返された値を表示します。
デバッグ状態を変更する要求も、同様の方法で処理されます。 たとえば、ブレークポイントを設定するという要求は、同様のステップで処理されます。もちろん、呼び出されるJDIメソッドや、送信されるJDWPコマンドや、呼び出されるJVM TI関数は違います。 さらに、フロントエンドとバックエンドは、単にデータをやり取りする以上のことを行います。アクティビティを追跡およびスケジューリングし、情報を変換、フィルタリング、およびキャッシュします。したがって、ブレークポイントを設定する要求は、値を取得する照会とはかなり違った仕方で処理されますが、通信の手順は同じです。
デバッグしているアプリケーションがこのブレークポイントに達すると、何が起こるのでしょうか。 今度は、イベントの出番になります。 仮想マシンは、JVM TIインタフェースを介してイベントを送ります。 具体的には、仮想マシンは、イベント処理関数を呼び出して、ブレークポイントを渡します。
バックエンドは、イベント処理関数を次のように設定しています。
static void Breakpoint(jvmtiEnv *jvmti_env,
JNIEnv* jni_env, jthread thread,
jmethodID method, jlocation location)
{ ...
このバックエンド関数は、関心のあるイベントをフィルタ・リングし、そのイベントをキューに入れ、ブレークポイント・イベント用に定義されたJDWP形式のソケットを介してそのイベントを送信するという、一連のアクティビティを開始します。 フロントエンドは、そのイベントをデコードして処理し、最終的にはJDIイベントを生成します。 特に、JDIイベントはそれをcom.sun.tools.jdi.event.BreakpointEvent
として公開しています。 その後、IDEは、そのイベントをイベント・キューから取り出して取得します。
theEventQueue.remove()
ここで、theEventQueue
はcom.sun.jdi.event.EventQueue
です。 IDEは、JDIを介して多くの照会呼出しを実行することにより、表示を更新すると予想されます。
移植
仮想マシンの各実装には、それぞれ独自のJVM TI実装が必要です。JVM TIの実装では、VMのデータ構造に深く踏み込む必要があり、イベントを取得するためにVM実装の中にフックを設定する必要があります。 JVM TIサポートのないVMにJVMDTを追加するのは、かなりの作業になります。 VMの複雑さと、実装するJVM TIのオプション機能の量に応じて、3か月から12か月のプロジェクトになると考えられます。 JVM TIサポートが組み込まれているVMを新しいプラットフォームに移殖する作業は、VMのJVM TI以外の部分の移殖が中心になります。JVM TIにかかる付加的な作業は比較的少量です。
バックエンドのリファレンス実装を新しいプラットフォームに移すには、多くの場合、ソースにわずかの変更(数行のみ)を加えるか、ソースをまったく変更せずに、再コンパイルするだけで済みます。 同じプラットフォーム上で新しいVMを使用する場合は、バックエンドのバイナリ・コードは多くの場合そのまま動作します。ただし、それはJavaプログラミング言語のコードではないため、内容を理解することはできません。 このドキュメントではライセンスの問題には触れていません。
フロントエンドの実装はJavaプログラミング言語で作成されているため、どのプラットフォームまたはVMでも動作します。 ただし、一部のシステムでは、コネクタ・コードの機能の一部を拡張する必要がある場合もあります。 たとえば、フロントエンドのリファレンス実装に含まれている起動ツールでは、仮想マシンがJava SEの規則を使って起動されることが前提です。 JDIのユーザーが自分たちの希望に合わせて起動ツールの構文を決めることもできますが、一般に、デバッガ・アプリケーションでは、その構文がJDI実装の側で決められていると想定します。 別の種類の通信チャネル(たとえば、シリアル接続)が必要な場合は、JDK 5.0で導入されたサービス・プロバイダ・インタフェースを使用して、その機能も追加する必要があります。