デバッガ:デバッガ一般

デバッガ:デバッガ一般

ここではデバッガの一般的であると思われる話題について広く浅く取り扱っています。


デバッガ4原則

最初にデバッガが守るべき原則を書いておきます。出典はデバッガの理論と実装

1、デバッグの手続きがシステムに過度の影響を与えてはならない

システムをデバッグするためにはシステム側に対する何らかの加工が必須となりますが、それによる影響を最小限にとどめなければなりません。デバッグしたいのはデバッガ上で動くアプリケーションではなく、普通の環境で動くアプリケーションです。普通に動かしたときに発現し、デバッグしている時は発現しないバグというのが最悪です。

2、デバッガは嘘をついてはならない

デバッグというのは、ありえない値やありえない挙動が見つかったときに得てして活躍するものです。そのため、その値が本当にありえない値(=デバッガによって捏造された値)であることはなんとしても避けなければなりません。それはユーザーを間違った方向に導くことになりますし、ユーザーのデバッガへの信頼を失わせることにもつながります。

端的に言うとこれは、取り繕ったり捏造したりしてはいけないということです。もしソースが最適化されていたら、それを素直に報告します。見せかける必要はどこにもありません。コールスタックが壊れていたら素直にそう表現しましょう。分からない値や壊れてしまった値でも同様です。信頼はデバッガにとってもっとも重要なものです。

3、文脈はデバッガにとってもっとも重要な情報です

ここでいう「文脈」とは、プログラマがもっとも必要とする情報=プログラムの文脈情報のことです。

バグが発生したとき、プログラマが一番知りたいのは「バグはどの場所で、どのような状況で発生したのか?」ということだと思います。例えば、例外が発生したら、その発生したときの情報を詳しく知りたいと思うはずです。具体的には

  1. どのファイルのどの行で例外が発生したのか?
  2. そのときの関数呼び出しの履歴はどうなっているのか?
  3. そのときのローカル変数やグローバル変数の値は?
  4. マルチプロセスならプロセス番号、マルチスレッドならスレッド番号は?
  5. [これは無くても構わないと思いますが] もっと具体的にどのCPUやVMのインストラクションで止まった? そのときのCPUやVMのレジスタの値は?

などの情報です。これらの情報をプログラマに指し示すことがデバッガのもっとも重要な仕事です。

4、デバッグはシステム開発の後を追う

大抵の場合、デバッグ機能というのは、そのシステムが開発されてからずっと後にならないと実装されません。これはつまり、そのシステムが開発されてからかなりの時間が経たないと、デバッガによる強力な支援が得られないことを意味します。例えばOSを見ても、Windows3.1には最初、デバッグAPIというもの自体が存在していませんでした。まともなデバッグ機能が付加されたのはWindows95になってからです。他の言語や今盛んなLL言語、大規模ライブラリなどを見てもこの傾向がうかがえます。

新技術や目新しい機能というのは割合すぐに実装されるものですが、それに対するデバッグやデバッグAPIが考慮されるのはそのずっと後です。 これにはいくつか理由が考えられるのですが、そのうちのいくつかには

というのが上げられると思います。したがってデバッガ開発者というのは常に、デバッグ対象となるシステム開発者やコミュニティに対して適切なデバッグAPIの提案と実装への要望を強めていく必要があります。

(注: ただし最近は最初からデバッグAPIが考慮された環境や言語も増えてきています)

IDEデバッガについて

とりあえず、IDEデバッガの紹介みたいなものを書いてみます。というのもIDEというものをほとんど使ったことが無い人もいると思いますし、普段使っていてもソースビューとかデバッガ・デバッギの区別をしている人などはおそらくほとんどいないと思われるからです。

もちろん、そういう環境を全く知らない人に想像しろというのも雲をつかむような話になってしまうので、 VisualStudioですがデバッガの参考例としてここに上げておきます。
イメージ画像(ちなみに、黄色い矢印で指されているのが実行中のソース行で、赤い丸はブレークポイントを示しています)

IDE環境に最低限必要なもの

他にもIDE環境にあったら嬉しいかもしれないもの

IDE環境の機能

デバッガの基本的なアーキテクチャ

ネイティブコードのデバッグでも、適切なサポートがされた環境(=.NETやJava、適切なデバッグAPIを備えたLL言語など)でのデバッグでも、仕組み自体にはそこまで大きな変化はありません。 ただ、難易度は大分変わってきます。

その前に用語説明をしておきます。

一般的なアーキテクチャ

デバッガのプログラムにはいくつかのパターンがありますが、そのメインループはおおむね同じような構造になることが多いようです。重要な違いとなるポイントは以下の2つです。

典型的なデバッガのプログラムを擬似コードを書くとこのようになります。

 (define (main-loop)
    ; もしデバッギメッセージがあるのならそれを処理します
    (when (check-debuggee-message)
       (handle-debuggee-message))
    ; もしGUIへのメッセージがあればそれを処理します
    (when (check-gui-message)
       (handle-gui-message))
    (main-loop))
 第一スレッド
 ; デバッギからの通知を待ち、来た場合はそれを処理します
 (define (debuggee-thread-loop)
    (wait-debuggee-event) ; デバッギからの通知を待ちます
    (post-event)          ; GUIに対してデバッギの状態をイベントとして通知
    (continue-debugee)    ; デバッギを再開
    (debuggee-thread-loop))
 
 第二スレッド
 ; GUIなどのメインループです
 (define (main-loop)
    (wait-message) ; メッセージを待ちます(ブロッキング呼び出し)
    (cond
     ((gui-message?) ...) ; GUIへのメッセージならそれを処理します
     ((debuggee-message?) ...)) ; デバッギメッセージならそれを処理します
    (main-loop))

インプロセスデバッガ

インプロセスデバッガとは、デバッガとデバッギが同一プロセス上にあるデバッガです。

Windows3.1や一部のスクリプト言語などではこの形を採用しています。 また他の形態の場合でも、インプロセスのスタブを挟むことによりインプロセスデバッガの利点を享受していることがあるようです。

  --------------------------------------         --------------------------------------
                                  関数コールバックなど
 
            デバッギ                  ←-→                  デバッガ
 
      (ブレークポイントや例外などの                 (表示やデバッギの制御などを担当)
    何か重要な出来事をデバッガに通知)
  --------------------------------------         --------------------------------------

アウトプロセスデバッガ

アウトプロセスデバッガとは、デバッガとデバッギのプロセス空間を完全に分離したデバッガです。

ただしデバッガによっては100%綺麗に分かれているわけではなく、一部の処理をデバッギのプロセス上で処理してしまうデバッガもあります。 Windows3.1以降のWindowsやLinuxなどではこの形態を採用しており、 dotnetではそのほとんどをアウトプロセスで処理しますが、一部の処理はインプロセスのヘルパースレッドに委譲しているようです。

  --------------------------------------         --------------------------------------
                                     プロセス間通信
 
            デバッギ                  ←-→                  デバッガ
 
      (ブレークポイントや例外などの                 (表示やデバッギの制御などを担当)
    何か重要な出来事をデバッガに通知)
  --------------------------------------         --------------------------------------

リモートデバッガの場合

リモートデバッガとはデバッギとデバッガを完全に分離し、別のマシン上でデバッグすることを可能にしたデバッガです。

Javaやeclipseではこのような形を採用しています。特にJavaの場合はこの形を必ず採用することが決まっており、 TCP/IP通信で使われるワイヤプロトコルの仕様も規格で定められています。

 ---------------------------       ---------------------------         ---------------------------
                 OSや環境に依存した何らかの通信        TCP/IPなどによる通信
 
         デバッギ            -→      スタブデバッガ          ←-→          デバッガ
                                                          (ワイヤプロトコル)
 
     (アプリケーション)            (デバッガとの通信などを担当)                (表示を担当)
 ---------------------------       ---------------------------         ---------------------------

デバッガの基本的なアーキテクチャ その2

アタッチとデタッチ

「すでに走っているプログラムに対してデバッグを開始できるのか?」というのは実は結構重要な問題で、 これができない場合、デバッガはデバッギの実行時からデバッギに対して何らかの関与をする必要があります。 このような場合、デバッガは何らかの特別なAPIを使いデバッギの実行を開始します。

多くのスクリプト言語ではアタッチとデタッチはできません。 これは大抵のスクリプト言語では、デバッガとの通信手段がインプロセスなものしかないことや そもそもそういうことをしたいことがあまり多くないというのが原因にあると思われます。 普通はスクリプトの開始直前に、デバッグ用のコールバック関数を設定しデバッギを開始します。

Windowsでは他プロセスへのアタッチができます。ただしデタッチはXP以上OSでないとできません。 デバッグを開始するには、新規プロセスを開始する場合にはCreateProcessというAPIに特殊なフラグを設定してデバッギを起動するか、 DebugActiveProcessというAPIを用いて既存プログラムにアタッチします。 デタッチにはDebugActiveProcessStopというAPIを使いますが、前述のとおりこれは新しいバージョンでしか使うことができません。

Javaの場合、デバッグAPIの仕様がリモートデバッグを前提とした仕様になっており、デバッグ対象となるデバッギは起動の段階で エージェント(前述の説明におけるスタブデバッガにあたるもの)とともに起動される必要があります。 エージェント付きで起動されたJavaプログラムには、デバッグAPIを通じて自由にアタッチ/デタッチを行うことができます。

デバッギとデバッガの通信手段

これにはいくつかの種類があります。代表的なものは以下のとおりです。

コードブレークポイント

コードブレークには大別して二つのやり方があります。

まず第一は、関数の呼び出しや新しい行の実行時など何か特定のイベントごとにデバッギからデバッガへ 何らかの通知を行う方法です。その時に実行コードの位置などを一緒に通知することで デバッガはそこがブレークポイントであるかどうかを判断することができます。 もしブレークする場合は、単純にデバッギへ制御を返さないことで実現できます。

第二の方法は、実行コードのブレークしたい場所に対して何らかのトラップコードを埋め込んでしまうという方法です。 トラップコードを検知したらデバッギはそれをデバッガに伝えます。 この方法を使うとデバッギの速度低下を最小限に抑えることが可能になります。

ネイティブコードの場合

ネイティブコードの場合、デバッガはCPU/OS/デバッグデータベースという三者の協力により実現しています。具体的には

  1. プロセッサの、トラップやフォールトの検出機構
  2. OSの、それらを捕まえデバッガに報告する機能
  3. デバッグシンボルテーブルから、シンボルや関数フレーム情報などを検索する機能

の三つの機構が必要になります。詳しくは後ほど紹介します。


Last modified : 2009/01/30 12:06:24 UTC