NetBeans on windowsでLinux上のrubyプログラムをリモートデバッグするためのproxy

NetBeansのリモートデバッグについて、以前の記事で、以下のように書きましたが、

注意点としては、リモートと同じフルパス上にソースコードを置く必要がある点です。リモート先で、/var/www/railsprjにRoRのプロジェクトコードが置いてあるなら、ローカルでも/var/www/railsprjにプロジェクトコードを置く必要があります。

このままだと、Windows上のNetBeansでLinux上のRailsがリモートデバッグできません。
そこで、NetBeansとFast Debugger(ruby-debug-ide)の間に挟むproxyを用意して、proxyの中でファイルパス変換することでリモートデバッグできるようにしました。本当は、NetBeansのIDE内部でファイルパス変換するように修正するべきだと思いますが、面倒だったのでproxyにしました。

せっかくなので、以下にproxyのソースコードを置いておきます(MIT Licenseにしときました)。

以下の構成で、基本的な機能(変数チェック、ステップ実行、breakpointセット辺り)が動作するのは確認しました。
Railsがデバッグできるかどうかは、まだ試してません。

[NetBeans (Debuger GUI)] on Windows
  ↓↑
[proxy] on Windows/Linux
  ↓↑
[ruby process (+ruby-debug-ide)]  on Linux

・CRuby 1.8.7 および JRuby 1.5.1でproxy動作確認した。

なお、実際にデバッグするときは、NetBeansが動くマシン上にもデバッグ対象のソースコードを置かないとソースコードデバッグできません。
例えば、ソースコードは以下のように配置します。

・ruby-debug-ide側
/home/user/script
|---test.rb
\---lib
      \---somelib.rb

・NetBeans側
C:\win_script
|---test.rb
\---lib
      \---somelib.rb


proxyを使ったデバッグの仕方


以下の手順でproxyの起動やNetBeansからproxyへの接続を行えば、後は普通にデバッグできます。

  1. ruby-debug-ide付きでRuby process起動(以前の記事参照)
  2. proxy起動(Linux上で起動するか、NetBeans on Windows上で起動するかはお好みで)。
  3. NetBeansでproxyへの接続。

1, 2の手順はどちらが先でもOKです。
1は以前の記事と同じなので、2, 3の説明だけ書きます。

2. proxy起動

proxyスクリプトをオプション指定して起動するだけです。proxyスクリプトのオプションは以下の通り。

Usage: ruby-debug-ide-proxy.rb -t  [-p listen_port] [--rdbprefix prefix] [--ideprefix prefix] [-d]

Example: ruby-debug-ide-proxy.rb -t localhost --rdbprefix "/home/user/script" --ideprefix "C:\\win_script" -d

Exampleの例を説明すると、

  • localhostのruby-debug-ideに接続して(接続先portは1234。指定するときはlocalhost:6000のようにする。)、
  • proxyを経由してNetBeansにデバッグデータ(XML)を送るときは、/home/user/script –> C:\\win_script への変換を行い、
  • proxyを経由してNetBeansからデバッグコマンド(breakpointセットなど)を指示するときは、C:\\win_script –> /home/user/scriptへの変換を行い、
  • proxy実行時は、DEBUGログを出力する

という指定になります。

3. NetBeansからproxyへの接続

NetBeansのメニューから「デバッグ -> デバッガを接続」を選択し、proxyが動作するホスト名やポート番号を指定して接続してください。
NetBeansからproxyに接続した時、proxyからruby-debug-ideへの接続が自動的に行われます。

正しくデバッガ接続できた場合、以下のようなログがproxyプログラムの端末に出力されます。

I, [2010-09-27T03:43:21.124000 #2240]  INFO -- : proxy will replace /home/user/script with C:\win_script
I, [2010-09-27T03:43:21.468000 #2240]  INFO -- : proxy listens on 0.0.0.0:7000
I, [2010-09-27T03:43:24.530000 #2240]  INFO -- : proxy connects to 192.168.1.7:1234
I, [2010-09-27T03:43:24.530000 #2240]  INFO -- : debug start

後は、普通にNetBeansのデバッガGUIが使えます。
例えば、デバッガGUIからbreakpointをセットすると、以下のログがproxy端末上に出ます。(-dを指定して起動した場合のみ)

D, [2010-09-27T03:43:24.546000 #2240] DEBUG -- : (ide -> proxy) b C:\win_script\test.rb:5
D, [2010-09-27T03:43:24.546000 #2240] DEBUG -- : (proxy -> rdb) b /home/user/script/test.rb:5
D, [2010-09-27T03:43:24.562000 #2240] DEBUG -- : (rdb -> proxy) 
D, [2010-09-27T03:43:24.624000 #2240] DEBUG -- : (proxy -> ide) 

手順は以上です。

Ruby/Railsの場合、デバッガはプログラムの動作理解に使うものというのが私の認識ですデバッグは、printfデバッグの方が効率がいいと思ってます。。なので、Ruby/Rails全般に慣れていない人がデバッガ利用者だと思うので、手順はもう少し簡潔にしたいですが、そうするにはIDE内を修正するしかないかな・・・。

NetBeansからRuby on Rails(RoR)のリモートデバッグ

NetBeansには、rubyのデバッガフロントエンドが同梱されています。
今回は、それを使ってRuby on Rails(以下RoR)のリモートデバッグをするためのメモです。

個人的には、RoRが動いてるマシンにsshでログインした後、emacsとrubydb3x.elを使う方がメリットがあるsshのポート(TCP 22)が開いてることは普通にありますが、デバッガ・デバッギ通信用のポートがfirewallで遮断されることは良くあります。と思いますが、emacsの場合デバッガ用GUIはありません。GUIに慣れてる人はNetBeansの方が楽なので、そういう人向けにまとめます。

なお、NetBeansをインストールしたローカルマシンで、RoRも動かす場合のデバッグ方法は、NetBeansのwikiを参照してください。

まあ、ローカルマシン上でデバッグする場合も、リモートマシンと接続してデバッグする場合もやり方はほとんど変わりません。

どちらの場合も、デバッギプロセス(と同じプロセス上で動くruby-debug-ide)がTCP(デフォルトだと1234)をListenし、Netbeansからそこに接続してデバッグを開始します。Netbeansから接続するデバッギプロセスがlocalhostにいるか、リモート先のホストにいるかの違いしかないです。
接続後、Netbeansからruby-debug-ideへbreakpointをしかける場所などを指示します。

デバッグ方法そのものは、ローカル・リモートともに一緒ですが、リモートのホストに対してローカルのNetBeansを接続してデバッグするには、リモート先のホストで以下の作業が必要です。

  1. rubyインストール (解説略)
  2. RubyGemsインストール (解説略)
  3. RoRインストール・プロジェクト作成 (解説略)
  4. ruby-debug-ideインストール
  5. リモート先のRoRプロジェクトのソースコードを、ローカルにも展開
  6. ruby-debug-ide付きでRoRプロジェクトのWebサーバ起動

4, 5, 6の手順は、あまりネット上で見かけないので順に解説します。

ruby-debug-ideインストール

Linuxだと、gemで簡単にインストールできます。(gcc, ruby-develをインストールしておけば。)
Fedora8だと以下で終わりでした。

$ gem install ruby-debug
$ gem install ruby-debug-ide

Windows(のmswin版ruby)の場合、コンパイル環境の用意が面倒なので、コンパイル済みgemをダウンロードした上でインストールします。最近だと、Visual C++は無料で手に入りますがmswin版rubyをコンパイルしたVisual C++と同じバージョンをインストールする必要しないと「MSC version unmatch」エラーが出てめんどくさいです。http://rubyforge.org/tracker/index.php?func=detail&aid=16774&group_id=1900&atid=7436

> gem install -l linecache-0.43-mswin32.gem
> gem install -l ruby-debug-base-0.10.3-mswin32.gem
> del ruby-debug-base-0.10.3-mswin32.gem
> gem install ruby-debug
> gem install ruby-debug-ide -v 0.4.6

コンパイル済みgem (linecache, ruby-debug-base)は以下2つのリンク先から
それぞれダウンロードしてください。

リモート先のRoRプロジェクトのソースコードを、ローカルにも展開

ローカルにもソースコードを置かないと、breakpointで止まった時にNetbeansにソースコードが読み込まれません。
注意点としては、リモートと同じフルパス上にソースコードを置く必要がある点です。リモート先で、/var/www/railsprjにRoRのプロジェクトコードが置いてあるなら、ローカルでも/var/www/railsprjにプロジェクトコードを置く必要があります。WindowsとLinuxだとフルパスを一致させることが不可能なので、例えば、Windows上のNetBeansからLinux上のRoRがデバッグができないという点でかなりアレな仕様です。誰もやってないなら修正パッチ作るかな。

この辺の挙動理解には、NetBeans wikiのChecking debugger engine functionalityとか、ruby-debug-ide protocolとかを参考にして、実際にruby-debug-ideとtelnetで会話するといいです。

ruby-debug-ide付きでRuby on RailsプロジェクトのWebサーバ起動

以下な感じでRoR Webサーバ起動します。(-dも付けると、NetBeansとの通信時の処理が良く見えます。)

$ rdebug-ide --stop script/server -h 0.0.0.0
Fast Debugger (ruby-debug-ide 0.4.6) listens on 0.0.0.0:1234

NetBeansからruby-debug-ideに接続

NetBeans IDEのメニュー -> デバッグ -> 接続をクリックし、ダイアログにリモート先ホスト名とポート番号(1234)を指定して接続します。
–stopを指定してあるので、RoRの処理先頭でstopするはずです。
あとは、IDEからbreakpoint指定するなり、なんなり自由に操作します。
watch変数として、paramsを追加しておくといい感じです。

ocamldebugの調査

ML言語の一種であるOcamlに付属している、ocamldebugというデバッガについて調べたのでメモ。

かなり間違っている可能性があるので、注意。

  • gdbライクなCUI
    • break,info,helpコマンド等、gdbを知っている人なら何となく使えるようにコマンド名がつけられてます
    • でも、printコマンドで関数呼び出しや代入が行えないなど*1、gdbと違う点もあるので注意
  • breakで停止できるのは、eventと呼ばれる箇所でのみ
  • debuggerとdebugeeは別プロセス
    • Unixだと、BSDソケットを使ってプロセス間通信することでdebugee上に存在する変数の値の参照等を行っている。ここらへんは、Javaデバッガの実装に近い
  • 日本語の処理関係は、まだ弱いらしい
  • mlコード内のprint_int, print_string等の標準出力への出力用関数の処理は、ocamldebugでステップ実行している間は実際には行われない
    • debugeeの実行が終了した段階で出力される
    • 出力をバッファしてるのかな?
    • おそらく、ファイル出力やTCP等によるデータ送信も同様
    • タイムトラベル機能(後述)のためにこのような実装になってる?
    • ファイルからの入力がどうなるかはまだ調べてない
  • 変数の値等の状態も含めて実行を前の地点まで巻き戻して、そこから再実行する機能がある(タイムトラベル機能)
    • ocamldebugが適切なタイミングでcheckpointを取り、それを利用して実行の巻き戻しを行う
    • 巻き戻した地点がcheckpointを取った地点と一致しない場合は、最寄のcheckpointからその地点まで再実行して停止?
    • checkpointを取るための実装にはforkを使ってる*2
    • プロセスIDが巻き戻す前と後で違うなど、完全に状態を巻き戻すことはできないため、いやらしいプログラムの実行を巻き戻すと挙動がおかしくなった

ocamldebugはOcamlで実装されているので、私には読めん・・・

*1http://caml.inria.fr/pub/docs/manual-ocaml.bak/manual030.htmlの16.7項に書かれているBNF記法を見る限りできないよなあ・・・たぶん

*2http://caml.inria.fr/pub/docs/manual-ocaml/manual030.htmlのTurning reverse execution on and offの項目に書いてある

デバッガの実装(C/C++編)

デバッガの実装について興味があるので調べてみました。

まずは、基本であるC/C++(さらには、アセンブラ)のデバッガについて。

単語の定義

  • debugger

デバッグを行うプロセス。debuggeeの実行制御等を行う

  • debuggee

デバッグ対象のプロセス

解説

一般的なOS(Unix, Windows等)では、セキュリティを守るためなどの目的で、OS上で動作しているアプリケーションのデータをプロセスごとに独立した仮想アドレス上に配置し、

CPUのレジスタも個々のプロセス*1ごとに管理します。

このため、debuggerがdebuggeeのレジスタ、メモリの値を読んだり、書き変えたりするには、

OSの力を借りる必要があります。

また、debugeeの実行中に発生するハードウェア、ソフトウェア割り込みを元にOSが生成する非同期イベント

(Unix/Linux系ではシグナル、Windowsでは構造化例外と呼ぶ。以下シグナル)の中には、

debuggeeが受け取る前に、debuggerが先に受け取って処理を行いたいシグナルが存在する*2ので、

debuggerはdebuggeeのシグナルをフックさせてくれとOSにお願いしなければいけません。

それ以外に、debuggeeの実行、終了、新規スレッドの追加、動的ライブラリのload, unloadなどのイベントも知りたいので、

これらのイベントもdebuggerに通知するようにOSにお願いする必要があります。

これらの依頼を行うために、OSから提供されるAPIをデバッグAPIと呼びます。

デバッグAPIの例

  • Linux
    • ptrace関数*3(PTRACE_ME, PTRACE_ATTACH, PTRACE_POKEDATA, PTRACE_PEEKDATA, PTRACE_GETREGS, PTRACE_SETREGS, …)
    • procファイルシステム
  • Windows
    • CreateProcess関数(の引数で渡す、DEBUG_PROCESSやDEBUG_ONLY_THIS_PROCESSフラグ)
    • DebugActiveProcess関数
    • Set(Get)ThreadContext関数(レジスタの値の読み書き)
    • Read(Write)ProcessMemory関数

たいていのOSのAPIは、

  1. debuggerがdebugeeプロセスを監視する権利を獲得し、debuggeeのシグナルやイベントの通知をOSから渡してもらえるようにする*4( PTRACE_ME, PTRACE_ATTACH, CreateProcess関数、DebugActiveProcess関数)
  2. レジスタやメモリの読み書きを行う

という手順で使われるようです。

ちなみに、各debuggeeの監視が行えるプロセスは1つしか許されないようで、

このことを利用したアンチデバッギング(プログラムの解析やチート行為などを回避するためにデバッグを防止すること)の手法がWizard Bible vol.32に書いてありました。

http://wizardbible.org/32/32.txt

breakpoint

breakpointには、主に2種類あります。

  • software breakpoint

debuggeeの中のbreakpointを仕掛けたい命令を、主にデバッグのために利用するCPUのソフトウェア割り込み命令(x86系CPUだとint 3命令)に書き換える。書き換えた割り込み命令が実行されたら、そのシグナルをdebuggerがキャッチして、仕掛けた割り込み命令を元の命令に書き換えてdebuggerのユーザの入力待ちに移行します

  • hardware breakpoint

CPUによっては、メモリのアドレスを格納し、このアドレスの命令やデータにアクセスする際に割り込みを発生させるCPUレジスタが存在する場合があります。これを使って割り込みを発生させ、その割り込みに対応するシグナルをdebuggerがキャッチして、debuggerのユーザの入力待ちに移行します

software breakpointの場合、debuggeeのメモリを書き換える必要があるのに対して、

hardware breakpointの場合、書き換える必要がないのがポイントです。

その他の機能

スタックトレースやステップ実行、ウォッチポイント等の機能は、レジスタやメモリの読み書きとbreakpointを応用することで実現できます。

C/C++のソースコードレベルでbreakpointを仕掛けたり、変数の値を見るためには、シンボルテーブル*5などを持つデバッグ情報(dwarf, stabs, coff等、複数の形式がある)が必要になります。

このあたりの話も書くと長くなるし、調べ切れてないので、いつか機会があったら書くことにします。

デバッガに関する話は、以下の本にかなり詳細に書かれてるので、興味を持った方は読んでみてください。

[amazon asin=’4756117457′ type=’banner’]

*1:正確には、プロセス内のスレッドごと

*2:たとえば、breakpointとして利用するソフトウェア割り込み(x86cpuの場合、int 3命令)

*3:関数じゃなくてシステムコールでした

*4:この段階で、debuggerはdebugeeの親プロセスになるようです(DebugActiveProcess関数を使ってアタッチした場合については、資料不足でよくわかりません・・・)

*5:変数名とそれに対応するアドレス情報のテーブル