CDT デバッガーとのインターフェース: 第 2 回 Eclipse の CDT と MI を使って gdb にアクセスする

CDT はどのように CDI を使って gdb の MI を扱うか

Eclipse の CDT (C/C++ Development Tooling) で提供されるグラフィカルなデバッグ環境は、ブレークポイント、ウォッチポイント、変数、レジスター、逆アセンブリー、シグナル、そしてメモリーなどの内容を表示でき、非常に素晴らしい環境になっています。この環境に機能を追加したり、あるいはこれらのビューにアクセスして出力を表示したりといったことを、カスタムのデバッガーから行うことができます。しかしまず、CDI (C/C++ Debugger Interface) を理解し、この CDI がどのように Eclipse と通信するかを理解する必要があります。この「CDT デバッガーとのインターフェース」シリーズの第 1 回では、この CDI を上位レベルで説明しました。この第 2 回では、CDT がどのように gdb (GNU Debugger) と通信を行うかを説明します。具体的には、CDT が CDI と MI (Machine Interface) をどのように使って gdb とインターフェースを取るのかを学びます。

Matthew Scarpino, Java Developer, Eclipse Engineering, LLC

Matthew Scarpino は、Eclipse Engineering LLC のプロジェクト・マネージャーであり、Java 開発者です。彼は SWT/JFace in Action の主執筆者であり、また SWT (Standard Widget Toolkit) に対して、小さいながらも重要な貢献をしました。彼が好きなものはアイルランドの民族音楽やマラソン、William Blake の詩、そして GEF (Graphical Editing Framework) です。



2008年 6月 24日

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) を理解する

大部分の人は、runprintinfo などの単純な命令を使ってコマンドラインから 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-slaveexecutable-name.

GDBCDIDebugger2 は実行中の gdb 実行可能プログラムに対する MIProcess を作成し、次に残りのデバッグ・プロセスを管理するための 2 つのセッション・オブジェクト (MISessionSession) を作成します。MISession オブジェクトは gdb への通信を管理し、Session オブジェクトは gdbセッションを「第 1 回」で説明した CDI に接続します。この記事のこれから先では、これらのセッション・オブジェクトについてを詳細に説明します。

MISession

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
  • ATxThread that sends commands from the outputCommandQueue to theOutputStream and an RxThread that sends receives commands from theInputStream and places them in the inputCommandQueue
  • gdb プロセスにデータを送信する OutputStream と gdb プロセスからの応答を受信する InputStream
  • MI コマンドを保持するための出力 CommandQueue と入力 CommandQueue
  • 出力 CommandQueue から OutputStream に対してコマンドを送信する TxThread と、InputStream からのコマンドを受信し、それらのコマンドを入力 CommandQueue に置く RxThread

これらのオブジェクトがどのように連携して動作するかは、例を見るとわかるでしょう。例えばデバッグ・セッションがリモートで行われる場合、CDT は remotebaud コマンドを gdb に送信し、それに続いてボー・レートを送信することで通信を開始します。これを実現するために、CDT は MISessionpostCommand メソッドを呼び出し、このメソッドが remotebaud コマンドをこのセッションの出力 CommandQueue に追加します。これによって TxThread がウェイクアップし、TxThread は gdb プロセスに接続された OutputStream に remotebaud コマンドを書き込みます。また TxThread はこのセッションの入力 CommandQueue にも remotebaud コマンドを追加します。

一方、RxThread は gdb プロセスからの InputStream を常に読み取っています。新しい出力が用意されると、RxThread はその出力を MIParser を介して送信し、結果レコードと OOB レコードを取得します。次に RxThread は入力 CommandQueue を検索し、その出力の元となった gdb コマンドを見つけます。RxThread は gdb の出力とその出力に対応するコマンドを理解すると、デバッガーの状態変化を知らせるために使われる MIEvent を作成します。

gdb との間でデータが送受信されると、TxThreadRxThreadMIEvent を作成して起動します。例えば TxThread はブレークポイントを変更するコマンドを gdb に送信する場合、MIBreakpointChangedEvent を作成します。RxThread は結果レコードが ^running である応答を gdb から受信する場合、MIRunningEvent を作成します。これらのイベントは「第 1 回」で説明した ICDIEvent インターフェースの実装ではありません。MIEventICDIEvent との関係を理解するためには、まず Session オブジェクトを理解する必要があります。

SessionTarget、そして EventManager

GDBCDIDebugger2 は MISession を作成した後、CDI の操作を管理する Session オブジェクトを作成します。Session オブジェクトは、そのコンストラクターが呼び出されると、Session 自身の管理動作を補助する多数のオブジェクトを作成します。なかでも次の 2 つのオブジェクトが特に重要です。そのオブジェクトとは、CDI モデルを管理しデバッガーにコマンドを送信する Target オブジェクトと、デバッガーが作成した MIEvent をリッスンする EventManager オブジェクトです。

第 1 回」で説明したとおり、Target は CDT からデバッグ・コマンドを受信し、それらのコマンドをデバッガー用にパッケージします。例えば Step Over ボタンをクリックすると、CDT は現在の Target を見つけて、その TargetstepOver メソッドを呼び出します。Target は応答として、MIExecNext コマンドを作成するとともに MISession.postCommand() を呼び出してこのステップを実行します。MISession がこの MIExecNext コマンドをその MISession 自身の出力 CommandQueue に追加すると、先ほど説明した方法でこのコマンドがデバッガーに転送されます。

MIEvent の中にパッケージされた gdb 出力は、そのセッションの EventManager によって受信されます。EventManager オブジェクトが作成されると、このオブジェクトは実行中の MISession に対する Observer として追加されます。MISessionMIEvent を起動すると、EventManager はそれらの MIEvent を解釈し、対応する ICDIEvents を作成します。例えば MISessionMIRegisterChangedEvent を起動すると、EventManagerChangedEvent という CDI イベントを作成します。EventManager はこの CDI イベントを作成した後、関係のあるすべてのリスナーに対して、状態変化が発生したことを通知します。これらのリスナーの多くは CDI のモデルの中の要素ですが、重要な例外として CDebugTarget と呼ばれるオブジェクトがあります。このオブジェクトは別のモデル階層構造の一部であり、次のセクションではこのオブジェクトについて説明します。


CDI と Eclipse のデバッグ・モデル

Eclipse のデバッグ・ビュー (Registers ビューや Variables ビューなど) とインターフェースを取るためのプラグインをデバッグする際には、Eclipse のルールに従う必要があります。つまり Eclipse のデバッグ・プラットフォームのイベントとモデル要素を使う必要があります。Eclipse のデバッグ・モデルのルート要素は IDebugTarget であり、他の要素としては IVariableIExpressionIThread などがあります。これらの名前がおなじみだとすると、それは 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;
}

CDebugTargetDebugEvents を作成することで CDI イベントに応答します (DebugEvent は一般的に、ステップ実行や実行の停止、再開などに関係します)。CDebugTarget はこれらのイベントを作成した後、Eclipse の DebugPlugin にアクセスし、このプラグインの fireDebugEventSet メソッドを呼び出します。これによって、状態変化が起きたことが Eclipse のすべてのデバッグ・リスナーに通知されます。つまり自分自身を DebugEventListener として追加するすべてのオブジェクトは DebugEvent を受信します。この中には Memory ビューや Variables ビューなど、Eclipse のデバッグ・ビューも含まれています。


CDT のデバッグ・ビュー

MI および CDI のラッパーと Eclipse との間の通信は、適切なデバッグ・データによって Eclipse のグラフィカル・ディスプレイを更新できて初めて有用なものとなります。図 1 はCDT のデバッグ・パースペクティブを示していますが、これを見るとターゲットの実行状態を表す多くのビューがあることがわかります。これらのビュー (Breakpoints や Modules、Expressions など) の多くは Eclipse によって提供されるものですが、CDT はこのパースペクティブに Executables ビューと Disassembly ビュー、そして Signals という 3 つのビューを追加しています。

図 1. CDT のデバッグ・パースペクティブ
CDT のデバッグ・パースペクティブ

これらのビューは同じような方法でデバッグ・イベントを作成し、受信します。このセクションでは Signals ビューについて説明します。上の図ではこの Signals ビューが最も上に表示されていますが、このビューにはターゲットが受信可能なシグナルがすべて一覧表示され、またどのシグナルをプロセスに渡せるかが表示されています。このビューが最初に表示される際には、SignalsViewContentProviderCDebugTarget を呼び出してシグナルの一覧を提供します。このターゲットは CDI のターゲットにアクセスし、このターゲットに対して、そのターゲットの CDI モデルの階層構造の中にあるシグナルを要求します。一連の ICDISignals が返されると、CDebugTarget は自分自身のモデル要素を更新してそれらの要素を SignalsViewContentProvider に送信し、SignalsViewContentProvider はそれらの要素を Signals ビューに追加します。

Signals ビューのエントリーを右クリックすると、Resume with Signal (シグナルにより再開) というコンテキスト・メニュー・オプションを使ってターゲットの実行を継続させ、選択されたシグナルをプロセスに送信することができます。このオプションは SignalsActionDelegate を呼び出します。このオプションが選択されると、SignalsActionDelegate は CDI のターゲットを呼び出し、選択されたシグナルに対応する ICDISignal を使ってそのターゲットの実行を再開します。ターゲットはそのシグナルに対する MI コマンドを作成して MISession.postCommand() を呼び出し、この MISession.postCommand() がそのコマンドを gdb に送信します。

gdb が応答する場合、Signals ビューを更新するためのプロセスには次の 5 つのステップがあります。

  1. MISession は gdb からの MI 出力を分析し、シグナルの設定が変更されているかどうかを判断します。変更されている場合には MISignalChangedEvent を起動します。
  2. CDI の EventManagerMISignalChangedEvent をリッスンし、ChangedEvent という CDI イベントを作成することで応答します。そして EventManager はこのイベントを起動し、すべての ICDIEventListeners に通知します。
  3. CDebugTarget はこのイベントを EventManager から受信し、この ChangedEvent がシグナルの変更に関係しているかどうかを判断します。関係している場合には CDebugTargetCDebugTargetCSignalManager を呼び出してこの CDI イベントを処理します。
  4. CSignalManager は自分のモデル要素を更新し、DebugEvent.CHANGE で指定される型の DebugEvent を起動します。
  5. SignalViewEventHandlerDebugEvent を受信し、このイベントがシグナルを扱っていることを確認し、Signals ビューを更新します。

Signals ビューに関係する操作を理解することが重要な理由は 2 つあります。つまり、これらの操作はさまざまなモデル要素がどのように連携して動作するかの具体例であり、また Eclipse と gdb、そして CDI と対話動作する同じようなビューを構成するための方法が、これらの操作の中に示されているからです。


まとめ

2 つのセッション・オブジェクト (MISessionSession)、2 つのターゲット (CDebugTargetTarget)、そして 2 つのまったく異なるモデル要素の階層構造など、CDT デバッガーの操作は非常に複雑なため、開発者の誰かが Rube Goldberg (訳注: 簡単にできることを多数のからくりが連鎖する複雑な装置で表現する方法を考えた、20 世紀の機械化を皮肉ったアメリカの新聞漫画家) と関係しているのではないかと思う人がいるかもしれません。それでも、CDT デバッガー用のコードはモジュール性を念頭に置いて作成されており、その内部動作をよく理解すればするほど、独自のモジュールを追加するのが容易になります。そして次のことを忘れないでください。CDT デバッガーの学習曲線は急峻ですが、CDT に新しい機能を追加する作業はゼロからカスタムのデバッグ・アプリケーションを作成するよりも遥かに容易なのです。


ダウンロード

内容ファイル名サイズ
Sample codeos-eclipse-cdt-debug-ex-debugger-plugin.zip15KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

  • Eclipse に関する質問を議論するための最初の場所として、Eclipse Platform newsgroups があります (このリンクをクリックすると、デフォルトの Usenet ニュース・リーダー・アプリケーションが起動し、eclipse.platform が開きます)。
  • Eclipse newsgroups には、Eclipse を利用し、拡張することに関心を持つ人達のために、さまざまなリソースが用意されています。
  • developerWorks blogs から developerWorks のコミュニティーに加わってください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=321994
ArticleTitle=CDT デバッガーとのインターフェース: 第 2 回 Eclipse の CDT と MI を使って gdb にアクセスする
publish-date=06242008