バグを潜伏させない工夫 - バグをいかに目立たせるか

とにかく目立て
Be visible
リチャード・ブランソン

一般的にバグの発生箇所と発現箇所が離れれていると、デバッグは難しくなり、時間もかかりがちです。今回のお話は、このような「バグの潜伏」を抑制し、「バグ」にいち早く気付かせるための実装上の工夫についてです。

ライブラリ設計において、何らかのオブジェクトにアクセスするIDを定義することがあります。例えば次のような関数定義があったとします。
file_id_t open_file(const char* path, int flags);
ssize_t   read_file(file_id_t id, char* buf, size_t size);
ssize_t   write_file(file_id_t id, const char* buf, size_t size);
off_t     seek_file(file_id_t id, off_t offset, int whence);
int       close_file(file_id id);
fileをオープンする関数によって返されたID(file_id_t型)が返され、以後のファイルアクセスはこのIDを介しての操作になっています。Linux等POSIX系システムで開発されている方はお気づきかと思いますが、上記の関数はPOSIXのI/O関連のsystem callのopen()/read()/write()/lseek()とほぼ同じ形をしています。POSIXでは、file_id_tに相当するのはint型のfile descriptor(fd)になります。POSIX系システムでは、ファイル以外にもsocketやデバイスドライバ等も同様のインターフェースでアクセスされます。POSIXで言えば、プロセスIDやユーザID等のIDもインターフェースに登場します

このようにopen等の関数で一旦捜査対象のIDを取得し、その後はそのIDで操作すると言うインターフェースは様々なところで見られます。このようなIDを用いたインターフェースの利点は何なのでしょうか?全てのインターフェースにファイル名等の文字列をそのまま用いる方法ではだめなのでしょうか?あるいは、IDではなくファイルオブジェクトへのポインタを返すというのでは駄目なのでしょうか?
IDを用いたインターフェースの利点は次のようなものが考えられます。
  • int型等の単純な型を用いることにより、対象の発見コストが低い
    (int型の比較と文字列方のstrcmpをイメージしてみて下さい)
  • IDの有効範囲を適切に設定すれば、プロセス間、システム間のやりとりが可能。
    ポインタでは同一メモリ空間内でしかやりとりできない。
上記の理由などにより、インターフェースにIDを利用するというのは一種の常套手段です。

前振りが長くなりましたが、今回の話題は「IDにどのような値を用いるべきか」という点です。
多くの場合、IDは「特定のオブジェクトを一意に指すもの」として定義される程度で、その値が具体的にどのような値を持つかという点については外部仕様として定義されないことも多いかと思います。IDはマジックナンバーとして利用されるものですので、利用する側が特定の値を期待してはいけません。そう考えると、IDの値について定義しないというのは順当だと考えられます。
外部仕様としてはIDの値について具体的な定義はしないものとしたとしても、実装側としてはどのような値を持たせるべきでしょうか。外部仕様を満たすだけであれば、何でも良いでしょう。例えば、
  • 0から順番にインクリメントした値を振っていく
  • 利用されなくなったIDは再利用する
というルールがまず思い浮かびます。Linux等のfile descriptorはこのような実装です。しかし、このような実装にはいくつかの欠点があります。それは、
  • 0は偶然利用される可能性が高い
  • IDの再利用は、IDの取得/解放シーケンスにバグがあった場合に、複雑なバグをまねきやすい
というものです。つまり、偶然それらしい値が指定されることで、中途半端な動作をまねき、バグが発見されにくくなるというものです。これらは、ライブラリ使用側が埋め込んだバグによって起こる問題で、ライブラリ作成者に責任はありません。しかし、IDの値をちょっと工夫してやれば、ライブラリ使用者にもっと早い段階でバグに気付くチャンスを与えることができます。例えば、次のようなルールを導入してみます。
  • 特殊な値、(0,1は~1(0xffffffff)等)はIDとして使用しない
  • IDは(極力)再利用しない
  • IDは1インクリメントで生成せず、2や3インクリメントなどで生成する
これによって、0や0xffffffffといった偶然入ってしまいがちなIDは指定されたば段階で間違いを指摘することが出来ます。また、IDの再利用をしないことで、シーケンスバグによってバグが複雑化することをさけることができます。最後の「1インクリメントでID生成しない」というのは、IDを配列の添え字のようなインデックス値と同様にインクリメントして使用してしまっているコードでの偶発動作を防ぎます。
どれも、偶然適当な値が入ることによって中途半端に動作が継続し、バグの発見が遅れたり、解析の手間が増えることを防ぐ効果があります。

と、ここまでIDの値という点で書きましたが、話の趣旨は次の二点に集約されます。
  • 外部仕様に出ない部分でも、ちょっとした工夫をするかしないかでデバッグ効率は大きく変わる
  • バグを含むコードであっても「偶然」や「なんとなく」で動作を継続させるとデバッグが難しくなる。 バグのある地点にできるだけ近い地点でバグに気付かせることで、デバッグ効率は上がる

ライブラリ設計において、機能性、拡張性、柔軟性等を考えることは勿論ですが、開発時にバグが混入し難い、バグ混入しても発見しやすい工夫といったことも頭に入れて置きたいですね。

【関連記事】
デバッグ指向のススメ



このエントリーをはてなブックマークに追加

コメントの投稿

非公開コメント

人気エントリ
最近の記事
本のおすすめ

4274065979

4844337858

482228493X

4904807057

4873114799


最近のコメント
Links
プロフィール
  • Author:proger
  • 組み込み関係で仕事してます
ブログ内検索