gdb (GNU Debugger) は最もよく使われているオープンソースのデバッガーです。gdb は元々 C 言語用に開発されたものですが、多くの言語のコードのデバッグ用に移植され、小型の組み込み機器から大規模なスーパーコンピューターまで、さまざまなコンピューター・システムで使われています。gdb は一般的にコマンドラインの実行可能プログラムとして使われていますが、あまり知られていない MI プロトコルを使用すると、ソフトウェアを介して gdb を利用することができます。この記事では MI の動作と、CDT がどのように MI を使って gdb と通信するかについて説明します。ここでは CDT デバッガーの対話動作の具体例を説明しますが、この例は CDT からカスタムの C/C++ デバッガーへのインターフェースを取ろうとする誰にとっても役立つはずです。
ここで説明する Java™ クラスは、この「CDT デバッガーとのインターフェース」シリーズの第 1 回で紹介した、CDI によって提供されるクラスとインターフェースの上に構築されています。混乱を避けるために、CDI と MI との間の違いを明確にしておきましょう。
- CDI (C/C++ Debugger Interface) は CDT が外部のデバッガーにアクセスできるように、Eclipse/CDT の開発者達によって作成されました。
- MI (Machine Interface) は外部アプリケーションが gdb にアクセスできるように、gdb 開発者達によって作成されました。
この違いは単純なように見えますが、ここで紹介するクラスの多くは CDI と MI の両方にまたがっており、一方のインターフェースがどこで終わり、もう一方のインターフェースがどこで始まるのかの判断が難しいことがあります。CDI と MI とがどのように連動するのかを理解できると、カスタムのデバッグ・ツールが gdb をベースにするか否かによらず、そのデバッグ・ツールと CDT とを適切にリンクさせることができます。
gdb/MI (GNU Debugger Machine Interface) を理解する
大部分の人は、run、print、info などの単純な命令を使ってコマンドラインから gdb にアクセスします。これは gdb に対する人間のインターフェースです。ソフトウェアを使って gdb とのインターフェースを取るために、gdb にアクセスする 2 番目の方法である MI (Machine Interface) が開発されました。gdb の動作はコマンドラインを使った場合と同じですが、コマンドと出力応答が大きく異なります。
これは例を見ると明らかです。例えば下記のコードに基づくアプリケーションをデバッグしたいとしましょう。
リスト 1. 単純な C アプリケーション: simple.c
int main() {
int x = 4;
x += 6; // x = 10
x *= 5; // x = 50
return (0);
}
|
このコードを gcc -g -O0 simple.c -o simple を使ってコンパイルすると、通常のデバッグ・セッションはリスト 2 のようなものになります。
リスト 2. デバッグ・セッション
$ gdb -q simple
(gdb) break main
(gdb) run
1 int main() {
(gdb) step
2 int x = 4;
(gdb) step
3 x += 6; // x = 10
(gdb) print x
$1 = 4
(gdb) step
4 x *= 5; // x = 50
(gdb) print x
$2 = 10
(gdb) quit
|
リスト 3 は、この同じ gdb セッションが MI コマンドを使うとどうなるかを示しています (MI コマンドは太字で示してあります)。
リスト 3. MI を使ったデバッグ・セッション
$ gdb -q -i mi simple
(gdb)
-break-insert-main
^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x00401075",
func="main",file="simple.c",fullname="/home/mscarpino/simple.c",line="1",times="0"}
(gdb)
-exec-run
^running
(gdb)
*stopped,reason="breakpoint-hit",bkptno="1",thread-id="1",frame={addr="0x00401075",
func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="1"}
(gdb)
-exec-step
^running
(gdb)
*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x0040107a",
func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="2"}
(gdb)
-exec-step
^running
(gdb)
*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x00401081",
func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="3"}
(gdb)
-var-create x_name * x
^done,name="x_name",numchild="0",type="int"
(gdb)
-var-evaluate-expression x_name
^done,value="4"
(gdb)
-exec-step
^running
(gdb)
*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x00401081",
func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="4"}
(gdb)
-var-update x_name
^done,changelist=[{name="x_name",in_scope="true",type_changed="false"}]
(gdb)
-var-evaluate-expression x_name
^done,value="10"
(gdb)
-var-delete x_name
^done,ndeleted="1"
(gdb)
-gdb-exit
|
-i mi flag フラグは MI プロトコルを使って通信するように gdb に指示しますが、これを見ると通常のデバッグ・セッションと MI を使った場合との違いの大きさがわかると思います。コマンド名が大幅に変わっており、また出力される内容も大幅に変わっています。出力レコードの最初の行は ^running または ^done であり、それに続いて結果の情報があります。この出力は結果レコードと呼ばれ、^error とエラー・メッセージを含む場合があります。
多くの場合、MI の結果レコードの後には (gdb) と OOB (out-of-band) レコードがあります。これらのレコードは、ターゲットまたはデバッグ環境の状態に関する追加の情報を提供します。-exec-step の後の *stopped メッセージは OOB レコードであり、ブレークポイントやウォッチポイントに関する情報や、なぜターゲットが停止または終了したかに関する情報を提供します。前のセッションでは、gdb はそれぞれの -exec-step の後に、ターゲットの状態と共に *stopped,reason="end-stepping-range" を返します。
gdb/MI は人間にとっては理解しにくいものですが、ソフトウェア・プロセスの間での通信には理想的です。CDT はこの通信を実現するために、データを送受信する pty (pseudo-terminal: 疑似ターミナル) を作成します。そして CDT は gdb を起動し、デバッグ・データを管理するための 2 つのセッション・オブジェクトを作成します。
「第 1 回」で説明したとおり、ユーザーが Debug をクリックすると、CDT は ICDebugger2 インスタンスにアクセスし、このインスタンスに対する呼び出しを行って ICDISession を作成します。このデバッガー・クラスは、org.eclipse.cdt.debug.core.CDebugger 拡張ポイントを継承するプラグインの中で識別されなければなりません。リスト 4 は、この拡張機能が CDT の中でどのように見えるかを示しています。
リスト 4. CDT のデフォルトのデバッガー拡張機能
<extension point="org.eclipse.cdt.debug.core.CDebugger">
<debugger
class="org.eclipse.cdt.debug.mi.core.GDBCDIDebugger2"
cpu="native"
id="org.eclipse.cdt.debug.mi.core.CDebuggerNew"
modes="run,core,attach"
name="gdb Debugger"
platform="*">
<buildIdPattern
pattern="cdt\.managedbuild\.config\.gnu\..*">
</buildIdPattern>
</debugger>
</extension>
|
このコードには、デバッグ・プロセスを開始する createSession() メソッドを GDBCDIDebugger2 が実装するということが記述されています。CDT がこのメソッドを呼び出す場合、CDT はデバッガーに起動オブジェクトを提供します (起動オブジェクトには構成パラメーターと、デバッグ対象の実行可能プログラムの名前 (executable-name)、そして進行状況表示モニターが含まれています)。GDBCDIDebugger2 はこの情報を使って、gdb 実行可能プログラムを起動する次のようなストリングを作成します。
gdb -q -nw -imi-version -ttypty-slave
executable-name
.
GDBCDIDebugger2 は実行中の gdb 実行可能プログラムに対する MIProcess を作成し、次に残りのデバッグ・プロセスを管理するための 2 つのセッション・オブジェクト (MISession と Session) を作成します。MISession オブジェクトは gdb への通信を管理し、Session オブジェクトは gdbセッションを「第 1 回」で説明した CDI に接続します。この記事のこれから先では、これらのセッション・オブジェクトについてを詳細に説明します。
GDBCDIDebugger2 は gdb を起動した後、最初に MISession オブジェクトを作成します。このオブジェクトは、次の 3 つのオブジェクト・ペアを使って gdb デバッガーへのすべてのアクセスを処理します。
- An to send data to the gdb process and an to receive its response
- An outgoing and incoming to hold MI commands
- A
TxThreadthat sends commands from the outputCommandQueueto theOutputStreamand anRxThreadthat sends receives commands from theInputStreamand places them in the inputCommandQueue
- gdb プロセスにデータを送信する
OutputStreamと gdb プロセスからの応答を受信するInputStream - MI コマンドを保持するための出力
CommandQueueと入力CommandQueue - 出力
CommandQueueからOutputStreamに対してコマンドを送信するTxThreadと、InputStreamからのコマンドを受信し、それらのコマンドを入力CommandQueueに置くRxThread
これらのオブジェクトがどのように連携して動作するかは、例を見るとわかるでしょう。例えばデバッグ・セッションがリモートで行われる場合、CDT は remotebaud コマンドを gdb に送信し、それに続いてボー・レートを送信することで通信を開始します。これを実現するために、CDT は MISession の postCommand メソッドを呼び出し、このメソッドが remotebaud コマンドをこのセッションの出力 CommandQueue に追加します。これによって TxThread がウェイクアップし、TxThread は gdb プロセスに接続された OutputStream に remotebaud コマンドを書き込みます。また TxThread はこのセッションの入力 CommandQueue にも remotebaud コマンドを追加します。
一方、RxThread は gdb プロセスからの InputStream を常に読み取っています。新しい出力が用意されると、RxThread はその出力を MIParser を介して送信し、結果レコードと OOB レコードを取得します。次に RxThread は入力 CommandQueue を検索し、その出力の元となった gdb コマンドを見つけます。RxThread は gdb の出力とその出力に対応するコマンドを理解すると、デバッガーの状態変化を知らせるために使われる MIEvent を作成します。
gdb との間でデータが送受信されると、TxThread と RxThread は MIEvent を作成して起動します。例えば TxThread はブレークポイントを変更するコマンドを gdb に送信する場合、MIBreakpointChangedEvent を作成します。RxThread は結果レコードが ^running である応答を gdb から受信する場合、MIRunningEvent を作成します。これらのイベントは「第 1 回」で説明した ICDIEvent インターフェースの実装ではありません。MIEvent と ICDIEvent との関係を理解するためには、まず Session オブジェクトを理解する必要があります。
Session と Target、そして EventManager
GDBCDIDebugger2 は MISession を作成した後、CDI の操作を管理する Session オブジェクトを作成します。Session オブジェクトは、そのコンストラクターが呼び出されると、Session 自身の管理動作を補助する多数のオブジェクトを作成します。なかでも次の 2 つのオブジェクトが特に重要です。そのオブジェクトとは、CDI モデルを管理しデバッガーにコマンドを送信する Target オブジェクトと、デバッガーが作成した MIEvent をリッスンする EventManager オブジェクトです。
「第 1 回」で説明したとおり、Target は CDT からデバッグ・コマンドを受信し、それらのコマンドをデバッガー用にパッケージします。例えば Step Over ボタンをクリックすると、CDT は現在の Target を見つけて、その Target の stepOver メソッドを呼び出します。Target は応答として、MIExecNext コマンドを作成するとともに MISession.postCommand() を呼び出してこのステップを実行します。MISession がこの MIExecNext コマンドをその MISession 自身の出力 CommandQueue に追加すると、先ほど説明した方法でこのコマンドがデバッガーに転送されます。
MIEvent の中にパッケージされた gdb 出力は、そのセッションの EventManager によって受信されます。EventManager オブジェクトが作成されると、このオブジェクトは実行中の MISession に対する Observer として追加されます。MISession が MIEvent を起動すると、EventManager はそれらの MIEvent を解釈し、対応する ICDIEvents を作成します。例えば MISession が MIRegisterChangedEvent を起動すると、EventManager は ChangedEvent という CDI イベントを作成します。EventManager はこの CDI イベントを作成した後、関係のあるすべてのリスナーに対して、状態変化が発生したことを通知します。これらのリスナーの多くは CDI のモデルの中の要素ですが、重要な例外として CDebugTarget と呼ばれるオブジェクトがあります。このオブジェクトは別のモデル階層構造の一部であり、次のセクションではこのオブジェクトについて説明します。
Eclipse のデバッグ・ビュー (Registers ビューや Variables ビューなど) とインターフェースを取るためのプラグインをデバッグする際には、Eclipse のルールに従う必要があります。つまり Eclipse のデバッグ・プラットフォームのイベントとモデル要素を使う必要があります。Eclipse のデバッグ・モデルのルート要素は IDebugTarget であり、他の要素としては IVariable や IExpression、IThread などがあります。これらの名前がおなじみだとすると、それは CDI のモデルの階層構造が Eclipse のデバッグ・モデルの階層構造を真似て構成されているためです。しかし CDI のモデルと Eclipse のデバッグ・モデルがお互いに直接通信することはできません。
そのため CDT には、CDI のクラスをラップして CDI のモデルと Eclipse のデバッグ・モデルとの間の橋渡しをする一連のクラスが含まれています。CDebugTarget はこのラッパー・モデルの階層構造のルートであり、CDI の EventManager によって起動されるイベントをリッスンします。CDebugTarget は新しいイベントを受信すると、大量の if 文と switch 文を処理して応答方法を判断します。例えば CDI イベントが ICDIResumedEvent だとすると、CDebugTarget はリスト 5 のコードを実行します。
リスト 5. CDI イベントを
DebugEvents に変換する
switch( event.getType() ) {
case ICDIResumedEvent.CONTINUE:
detail = DebugEvent.CLIENT_REQUEST;
break;
case ICDIResumedEvent.STEP_INTO:
case ICDIResumedEvent.STEP_INTO_INSTRUCTION:
detail = DebugEvent.STEP_INTO;
break;
case ICDIResumedEvent.STEP_OVER:
case ICDIResumedEvent.STEP_OVER_INSTRUCTION:
detail = DebugEvent.STEP_OVER;
break;
case ICDIResumedEvent.STEP_RETURN:
detail = DebugEvent.STEP_RETURN;
break;
}
|
CDebugTarget は DebugEvents を作成することで CDI イベントに応答します (DebugEvent は一般的に、ステップ実行や実行の停止、再開などに関係します)。CDebugTarget はこれらのイベントを作成した後、Eclipse の DebugPlugin にアクセスし、このプラグインの fireDebugEventSet メソッドを呼び出します。これによって、状態変化が起きたことが Eclipse のすべてのデバッグ・リスナーに通知されます。つまり自分自身を DebugEventListener として追加するすべてのオブジェクトは DebugEvent を受信します。この中には Memory ビューや Variables ビューなど、Eclipse のデバッグ・ビューも含まれています。
MI および CDI のラッパーと Eclipse との間の通信は、適切なデバッグ・データによって Eclipse のグラフィカル・ディスプレイを更新できて初めて有用なものとなります。図 1 はCDT のデバッグ・パースペクティブを示していますが、これを見るとターゲットの実行状態を表す多くのビューがあることがわかります。これらのビュー (Breakpoints や Modules、Expressions など) の多くは Eclipse によって提供されるものですが、CDT はこのパースペクティブに Executables ビューと Disassembly ビュー、そして Signals という 3 つのビューを追加しています。
図 1. CDT のデバッグ・パースペクティブ
これらのビューは同じような方法でデバッグ・イベントを作成し、受信します。このセクションでは Signals ビューについて説明します。上の図ではこの Signals ビューが最も上に表示されていますが、このビューにはターゲットが受信可能なシグナルがすべて一覧表示され、またどのシグナルをプロセスに渡せるかが表示されています。このビューが最初に表示される際には、SignalsViewContentProvider は CDebugTarget を呼び出してシグナルの一覧を提供します。このターゲットは CDI のターゲットにアクセスし、このターゲットに対して、そのターゲットの CDI モデルの階層構造の中にあるシグナルを要求します。一連の ICDISignals が返されると、CDebugTarget は自分自身のモデル要素を更新してそれらの要素を SignalsViewContentProvider に送信し、SignalsViewContentProvider はそれらの要素を Signals ビューに追加します。
Signals ビューのエントリーを右クリックすると、Resume with Signal (シグナルにより再開) というコンテキスト・メニュー・オプションを使ってターゲットの実行を継続させ、選択されたシグナルをプロセスに送信することができます。このオプションは SignalsActionDelegate を呼び出します。このオプションが選択されると、SignalsActionDelegate は CDI のターゲットを呼び出し、選択されたシグナルに対応する ICDISignal を使ってそのターゲットの実行を再開します。ターゲットはそのシグナルに対する MI コマンドを作成して MISession.postCommand() を呼び出し、この MISession.postCommand() がそのコマンドを gdb に送信します。
gdb が応答する場合、Signals ビューを更新するためのプロセスには次の 5 つのステップがあります。
-
MISessionは gdb からの MI 出力を分析し、シグナルの設定が変更されているかどうかを判断します。変更されている場合にはMISignalChangedEventを起動します。 - CDI の
EventManagerはMISignalChangedEventをリッスンし、ChangedEventという CDI イベントを作成することで応答します。そしてEventManagerはこのイベントを起動し、すべてのICDIEventListenersに通知します。 -
CDebugTargetはこのイベントをEventManagerから受信し、このChangedEventがシグナルの変更に関係しているかどうかを判断します。関係している場合にはCDebugTargetはCDebugTargetのCSignalManagerを呼び出してこの CDI イベントを処理します。 -
CSignalManagerは自分のモデル要素を更新し、DebugEvent.CHANGEで指定される型のDebugEventを起動します。 -
SignalViewEventHandlerはDebugEventを受信し、このイベントがシグナルを扱っていることを確認し、Signals ビューを更新します。
Signals ビューに関係する操作を理解することが重要な理由は 2 つあります。つまり、これらの操作はさまざまなモデル要素がどのように連携して動作するかの具体例であり、また Eclipse と gdb、そして CDI と対話動作する同じようなビューを構成するための方法が、これらの操作の中に示されているからです。
2 つのセッション・オブジェクト (MISession と Session)、2 つのターゲット (CDebugTarget と Target)、そして 2 つのまったく異なるモデル要素の階層構造など、CDT デバッガーの操作は非常に複雑なため、開発者の誰かが Rube Goldberg (訳注: 簡単にできることを多数のからくりが連鎖する複雑な装置で表現する方法を考えた、20 世紀の機械化を皮肉ったアメリカの新聞漫画家) と関係しているのではないかと思う人がいるかもしれません。それでも、CDT デバッガー用のコードはモジュール性を念頭に置いて作成されており、その内部動作をよく理解すればするほど、独自のモジュールを追加するのが容易になります。そして次のことを忘れないでください。CDT デバッガーの学習曲線は急峻ですが、CDT に新しい機能を追加する作業はゼロからカスタムのデバッグ・アプリケーションを作成するよりも遥かに容易なのです。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample code | os-eclipse-cdt-debug-ex-debugger-plugin.zip | 15KB | HTTP |
学ぶために
-
Eclipse.org の Eclipse CDT を訪れてください。
-
CDT プロジェクトのリーダーのブログを読んでください。
- 「Eclipse の推奨読み物リスト」を調べてみてください。
- developerWorks には他にも Eclipse に関する話題が豊富に用意されています。
- Eclipse が初めての人は、developerWorks の記事「Eclipse Platform 入門」を読んでください。Eclipse の起源やアーキテクチャー、またプラグインを使って Eclipse を拡張する方法などを学ぶことができます。
- IBM developerWorks の Eclipse project resources を利用して Eclipse のスキルを磨いてください。
-
developerWorks podcasts では、ソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
-
developerWorks の Technical events and webcasts で最新情報を入手してください。
- IBM とオープンソース技術、そして製品機能を調べ、学ぶために、無料の developerWorks On demand demos をご覧ください。
- IBM オープンソース開発者にとって関心のある、世界中で今後開催される会議や業界展示会、ウェブキャスト、その他のイベントについて調べてみてください。
-
developerWorks の Open source ゾーンをご覧ください。オープンソース技術を使った開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
製品や技術を入手するために
- IBM alphaWorks に用意された最新の Eclipse technology downloads を調べてください。
- Eclipse Foundation から Eclipse Platform やその他のプロジェクトをダウンロードしてください。
-
IBM 製品の試用版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。
- 皆さんの次期オープンソース開発プロジェクトを IBM trial software で革新してください。ダウンロード、あるいは DVD で入手することができます。
議論するために
- Eclipse に関する質問を議論するための最初の場所として、Eclipse Platform newsgroups があります (このリンクをクリックすると、デフォルトの Usenet ニュース・リーダー・アプリケーションが起動し、eclipse.platform が開きます)。
-
Eclipse newsgroups には、Eclipse を利用し、拡張することに関心を持つ人達のために、さまざまなリソースが用意されています。
-
developerWorks blogs から developerWorks のコミュニティーに加わってください。