デバッグ指向のススメ

予めおもんぱかれば、簡単であるが、後になっておもんぱかれば、複雑になる。
ゲーテ -『格言と反省
デバッグ指向プログラミング(debug-oriented programming)とは何か。一言で言うとすると、「デバッグしやすいプログラムを書きましょう」ということです。テスト指向(test-oriented)と近いところもありますが、よりデバッグの効率に重点を置いた考え方です。詳しい内容は今回だけでは書ききれませんが、とりあえず今回は、なぜデバッグ指向なのか、デバッグ指向とは具体的にどんなものか、ということについて書いてみたいと思います。

バグの発生は必然

バグの発生は予定外の出来事ではありません。よほど小規模のプログラムでない限り、ソースコードが一発で不具合なく動くことはほとんどありません。つまり、デバッグ作業というものは、ソフトウェア開発において重要なステップなのです。実際、大抵のソフトウェア開発では、コーディングそのものよりもデバッグに費やす時間の方が多いことが多いのではないでしょうか。つまり、このデバッグをいかに効率よく進められるかということが、ソフトウェア開発の効率を左右するといっても過言ではありません。

デバッグ効率をいかにあげるか

では、デバッグの効率はどのようにすれば改善できるのでしょうか?項目としてあげるとすると、
(1)バグのパターンを知り、解析・切り分け方法を習得する
(2)デバッガ・各種ツールを使いこなす
(3)デバッグ用のコードを埋め込んでおく
(4)デバッグしやすいコードを書く
といったところでしょうか。

(1)についてですが、設計手法と同様、デバッグ手法にもパターンや系統的手法が存在します。よくあるバグのパターンを知っていると、デバッグ効率もあがります。
(2)についてですが、最近のデバッガはかなり強力です。メモリリークやオーバーランの検出に有用なツールのあります。デバッガやツールを使いこなせるかどうかでデバッグ効率は大きく変わってきます。

(3)(4)が、デバッグ指向プログラミングの勘所です。デバッグ用のコードといっても単にprintfを埋め込めというわけではありません。勿論ログを残すというのは有効なデバッグ方法ですが、それだけではなく、assertの埋め込み、デバッガ向けコード、デバッグ関数の埋め込み等があります。開発対象がライブラリであれば、ライブラリ自身だけでなく呼び出し側でのデバッグも考慮します。

細かい点は追々追加していきますので、今回はデバッグ指向の一例として、契約プログラミングについて説明します。

契約プログラミング

契約プログラミング(DBC:Design By Contract)という言葉をご存知でしょうか。プログラムのコード上に、満たすべき条件を埋め込んでおき、違反があれば検出するというものです。最も基本的なものにassertがあります。ご存知のように、条件をチェックして、不一致であればログ出力後、abortするものです。
#include <stdio.h>
#include <assert>

#define ENTRY_MAX 5
int g_entry[ENTRY_MAX]

int
nth_entry(int n)
{
  assert(n < ENTRY_MAX && n >= 0)
  return g_entry[n];
}

上記のコードでは、nが5以上で呼び出されると、abortしてプログラムが停止してしまいます。違反が検出された時点でプログラムが停止するため、デバッガで動作させていればすぐにバックトレースをとることもできます。

では、assertがなかった場合、どうなるでしょうか?運がよければ、不正メモリアクセスでsegmentation faultか何かで同じく停止し、assert使用時と同様に発生地点を発見できるかもしれません。しかし、大抵の場合、不定値を返したあと、しばらく実行が続いたのちにバグが表面化するため、一体どこでバグが入ったのかを調べるのが大変になります。バグは発生した瞬間に捕まえないと非常にやっかいなので、assertで異常を見つけた瞬間に停止させるのは非常に有効な手段です。

まれにつぎのようなコードをみかけます。
#include <stdio.h>
#include <assert>

#define ENTRY_MAX 5
int g_entry[ENTRY_MAX]

int
nth_entry(int n)
{
  if (n >= ENTRY_MAX || n < 0)
    return -1; /* out of range */
  return g_entry[n];
}

配列の範囲外アクセスの場合、エラー(-1)を返すということになっています。外部提供関数であれば、これもありかもしれませんが、これが内部関数であれば、エラーを返す意味がありません。内部ロジックがおかしいのですから、エラーなぞ返さずに異常検出でabortした方が望ましいのです。誰もエラーチェックしないような関数でエラーを返す意味はありません。先のassertを入れたコードのようにするべきでしょう。

assertは非常に有用です。必ず満たしていなければならない条件がある場合は、こまめにassertをいれるべきです。関数の引数だけでなく、複雑なアルゴリズムの計算経過の確認などでも使えます。assertはコメントと違い、直接コードとリンクするのでコードとの不一致を起こすこともありません。

但し、assertを入れるとそのチェックが入る分、コードサイズや処理時間に影響を及ぼします。ですので、数が多い場合は、段階ごとにassertをOFFにできるようしておいた方が良いでしょう。
#define DEBUG_LEVEL 0

#if DEBUG_LEVEL > 0
#define ASSERT_1(x) assert(x)
#else
#define ASSERT_1(x)
#endif

#if DEBUG_LEVEL > 1
#define ASSERT_2(x) assert(x)
#else
#define ASSERT_2(x)
#endif

こうすれば、DEBUG_LEVELマクロで、一部のassertだけを有効にできます。

今回は契約プログラミング,assertについて説明しました。その他のデバッグ指向プログラミングに関するTIPSも本サイトでおいおい紹介していきたいと思います。ちなみに、「デバッグ指向」という単語は私の造語ですので、Googleで調べていただいてもhitしません。あしからずご了承ください (^^;)

※ D言語ではより強化された契約プログラミングの機能が言語レベルで実装されているそうです

4873114063 【関連記事】
バグのパターン

【関連リンク】
Linuxのデバッグ手法をマスターする
プログラミング言語D - 契約プログラミング

【関連書籍】
.NET&Windowsプログラマのためのデバッグテクニック徹底解説
Javaデバッグ明快技法
D言語パーフェクトガイド―一歩先行くC/C++/C#/Java開発者に贈るD

コメント

[OR] では・・?

nth_entry(int n) の説明で気になったのですが、
---------------------------------
★if (n >= ENTRY_MAX && n < 0)★
return -1; /* out of range */
---------------------------------

★の所は
if (n >= ENTRY_MAX || n < 0)
ではないですか?おそらく。

修正しました

ご指摘ありがとうございます。
修正しました(^^;)
非公開コメント

本のおすすめ

4274065979

4844337858

482228493X

4904807057

4873114799


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索