Entry

プログラミングメモ - コーディングのノウハウも大切だけど典型的なバグを知ることも大事

2009年09月26日

あまりネタがないので,少しこの頃思っていることをば。

プログラミングの話を書いたり読んだりしていると,人気があるエントリとそれほど人気のないエントリに分かれることに気が付きます。

人気があるエントリは,言うまでもなく「新しいものを作る」エントリ。例えば,新しいプログラミング手法の話とか,新しい設計手法とか,新しいパラダイムとかの話です。新しいものを作るときは,多くの場合,作りたいモノが動く仕組み(アルゴリズム)とプログラミング言語の言語仕様をだけ知ってれば作れちゃう。やったことに対する成果が分かりやすいから,楽しいに決まってます。

一方で,作った後の話,例えばデバッギングの話なんかは,この頃少し注目されるようになったものの,全体として見るとあまり人気はありません。デバッグするには,アルゴリズムや言語仕様以外にも,ハードウェアの知識や OS の知識といった各知識を総合的に動員しなくちゃいけません。その割に,デバッグの成果は,マイナスがゼロになるといった消極的なもの。達成感がない。職業技術者で必要性を感じている人でもない限り,新規開発の話に食指が伸びるのは,当然っちゃ当然の話です。

また,せっかく書かれたデバッグ話でも,今のところ,デバッガの使い方や総論的な心構えの話が多くて,本当に厳しい各論的局面について語られることは少なかったりもします。こゆところが,本当は必要なんですけどね。デバッギングの実際はケース・バイ・ケースになりがちだから,まとめるのが難しいってのもあるんですけどね。なんとか,うまい具合にまとめてくれている方がいるとうれしいな,と(ひとだのみ)

ま,能書きたれてても仕方ないので,せっかくだから厳しい局面になりがちなバグの例を挙げてみます。例えば,よくあるバグの類型のひとつとして,次のようなコードがあります。

#include <iostream>

#define TABLESIZE 10

void
setNumber(int evenTable[]) {
    for (size_t i = 0; i <= TABLESIZE; ++i) {
        evenTable[i] = (i << 1);
    }
}

int
main(int argc, char argv[]) {
    int evenTable[TABLESIZE];
    int someVariable = 0;

    setNumber(evenTable);
    std::cout << someVariable << std::endl;

    return 0;
}

これ,実行すると「20」と表示されます。someVariable の値は 0 で初期化しているはずなので,20 と表示されるののはおかしいですよね。バグがあります。どこにあるか分かるでしょうか?

お分かりの通り,setNumber() 関数内の for-文の終了条件が "<=" になっているので,ループが1回多く回っているのが原因です。10回だけ回すはずなのに,11回クルクルやっている。いわゆる バッファ・オーバーランというやつです。バッファ・オーバーランというと,プログラムが落ちるイメージがあるけれども,この場合は何度やっても絶対に落ちません。

どうしてかというと,1回多く回った分の値が,その次に定義されている変数 someVariable に代入されるからです。スタック領域のアドレスを渡しているので,これはバッファ・オーバーランでスタック破壊も起こしていることになります(スタック破壊は通常関数の戻りアドレスを破壊することを指すけれども,これも大きな意味ではスタック破壊(と考えてください))。よく見るバグなんですけれど,一見落ちずに動く点から言っても,どこで誤った値が入っているか分かりづらい点から言っても,原因を突き止めるのが難しいバグになります。

もちろん,ここではバグのある部分だけを強調しているので,まだ分かりやすいはずです。実際のコードでは,バッファに対して外部から入ってきた文字列を sprintf(3) する場合や strcat(3) する場合,さらには,バッファに収めるデータの長さを計算する式が複雑な場合,バッファへのポインタが複数の関数やクラスを渡る場合なんかがあるわけで。こうなると,どこが直接の原因か一発で突き止めるのはかなり難しい。

この点,バッファ・オーバーフローやメモリリークといった,典型的なバグの場合,対処するための仕組みや製品が用意されていたりします。デバッグの総論としてこれらの存在について知ることは,もちろん重要で,余裕があったら導入した方がいい。

しかし,もっと重要なことは,こうした典型的なバグに対処するための,現場におけるイディオムだと思うんです。便利だからといってあれもこれもと検知機構を利用していると,問題の本質をないがしろにする恐れがあると思うからです。バグのメカニズムを知っていれば,少しのテストコードを埋め込むだけで,これらに対処できる場合もあるわけで,こゆ方法を臨機応変に使えることも大事だと思うわけです。

例えば,上のようなコードの場合,自前でカナリアコードを書いてもいい。

int
main(int argc, char argv[]) {
    int evenTable[TABLESIZE];
#if defined(_DEBUG)
    int dummy[100] = {0xCD};  // カナリア
#endif
    int someVariable = 0;

    setNumber(evenTable);
    std::cout << someVariable << std::endl;

#if defined(_DEBUG)
    assert(dummy[0] == 0xCD);
#endif

    return 0;
}

「カナリアライブラリがないからデバッグできません」とか言ってたら,トホホですしね。数分で試せるテストコードくらい,ちょろっと書けるノウハウが欲しいところ。デバッギングは,ツールを使えば全部解決するというわけではなくて,バグの原因に対する理解と,それに対応する能力,そして二度と繰り返さないように検知するノウハウを総合する必要があるはずです。そういう蓄積があればいいなあ,と思うわけです。

この頃は,IT 周りの浮いた話は多いものの(昔から多いが),ベテランさんがあまりネット上にコードを書かなくなってしまったようで,こうしたノウハウはむしろ少なくなっているようにも思います。実は大切なんですけどね。地味なんだけど。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN