この記事は、『UNIX Magazine』2003年10月号(2003年9月18日発売)に掲載された同名記事の初稿(著者から編集部に提出したもの)を元に、Web掲載用に一部を修正したものです。10年以上前に執筆したものなので、現在のUNIXを取り巻く環境とは色々と異なることがありますが、プログラミングに対する心構えとしては現在でも通用するものと思い、再掲してみることにしました。
・・・・・・・・・・
プログラムのデバッグについて、実際の開発に役立つ事柄が書かれた書籍や記事には、あまりお目にかかったことがない。では、「おまえは書けるのか」と問われれば、やはり書けないと答えざるを得ない。
もし、今ここに現実のバグ現象があり、これを一体どのように克服すべきか、という議論を開発担当者と膝を付き合わせてやる、ということであれば、いくらでも話はできる。しかし、あらたまって「デバッグとは」と始めると、途端に頭が回らなくなる。どうも、デバッグに関しては、局所戦の戦術は論じられても、大局的戦略を論じられるほど頭が練れていないようだ。
とは言え、デバッグについても書いておきたいことがいくらかはある。断片的で、多少の矛盾は生じてしまうと思うが、バグと戦う際のヒントとして読んでいただければと思う。
動くべくして動く¶
筆者は、一緒にプログラム開発を行う技術者に、次のような問いかけをすることが良くある。
それは 動くべくして 動いているのか?それとも たまたま 動いているのか?
驚くべきことに、世の中の実に多くのプログラムが「たまたま」動いているのだ。この「たまたま」動いている部分が、ごく希に周囲の環境の変動などに揺さぶられて悪い方へ転び、動作不良などの現象となって顕在化すると「バグ」と呼ばれることになる。デバッグとは、この「たまたま」動いているプログラムを、「動くべくして」動くプログラムに正す行為である、と言うこともできる。
最初から、プログラムを「たまたま」動くように書くことは、ほぼあり得ない。書く時点では、程度の差はあれ「動くべくして」動くように書くはずである。しかし、バグが皆無のプログラムを作ることは恐ろしく難しいことなどを勘案すると、多くのプログラムがやはりどこか「たまたま」動いているという状態のままなのだ。
動いていたのですが……¶
開発者に、不具合が生じたプログラムの動作状況を説明してもらうと、「これこれこういう状況では動いていたのですが……」という、半ば言い訳的な説明をされることが良くある。取引先との儀礼的・政治的なやり取りでは、まあやむを得ない面もあるが、開発者本人が心底そう思っているとしたら、これは問題である。
「こういう状況では動いていたのに……」という感覚が強くある、ということは、「動作不良は特別な状態であり、本来自分のプログラムは間違っていない」という意識が根底にある、ということでもある。間違っていない、と思っている中から、誤りを探すのは実に難しい。すなわち、このような感覚が強くある人は、効果的で正しいデバッグはできないのだ。
プログラムは「動くべくして動く」ように書かねばならない。しかし、自分の知識・技量・経験には限りがあり、事前に全ての可能性を見通すことはできないのだから、書いたプログラムが「たまたま」動いている状態である可能性は十分にある。そして、不具合が見つかったということは、「たまたま」動いている状態であることが証明された、ということであり、それを謙虚に受け入れ、誤りを探して正さねばならない、と心の底から考えられるようになれば、効果的で正しいデバッグができるようになるだろう。
不具合現象を消す¶
比較的経験が浅い技術者のデバッグのやり方を見ていると、とにかく 不具合現象を消す ことを至上命題として全力で取り組む傾向が見て取れる。不具合現象とは、大雑把に言えば「想定外の挙動」であるから、「不具合現象を消す」とは、「想定外の挙動を、想定された挙動に変える」ということになる。
たとえば、あるプログラムに特定の入力を与えた時、整数値0の出力が想定されるとしよう。しかし、実際には整数値1の出力が得られたとすると、これは想定外の挙動であって、不具合現象(いわゆるバグ)である。この現象に対して、「どうしたら0が出力されるようになるのか?」という態度で挑むのが、「不具合現象を消す」というアプローチである。
プログラム内の関係のありそうな箇所や、少し気になっている箇所を、いくつかチョイチョイといじってみたりして、再コンパイルして実行してみる。そうすると、何かの拍子にふと整数値0が出力されるようになる。そこで「直った!」となるわけだ。これは、本当に実際に良く見受けられるデバッグ方法だ。
言うまでもなく、これは非常にまずい。チョイチョイといじって「直った!」としてしまうデバッグ方法にも問題はあるが、より深刻なのは、そもそも「想定された挙動」自体が本当に正しいかどうか、全く考察していないということだ。
原因の究明こそがデバッグ¶
経験豊富な技術者は、決して不具合現象を消すという態度は取らない。不具合現象は、プログラムに内在する広義の誤りを正すことによって、結果として必ず消えるものである。故に、まず全力で取り組むべきは、不具合現象の生じる 原因の究明 である。つまり「1が出力されるということは、一体何が起きているのか?」を徹底的に追求するのだ。
原因究明の結果、プログラムの記述誤りや計算式の誤りなどが発見され、その誤った処理においては、確かに与えられた特定の入力について整数値1が出力されることが証明できるのであれば、原因の究明は終了である。この原因に対する対策は、「誤りを正す」ことであり、それにより不具合現象は消え、想定どおりに整数値0が出力されるようになる。
しかし、世の中はそう単純なものではない。実は、プログラム設計上の仕様では、確かに与えられた特定の入力で整数値1が出力されてしまう、つまり、プログラムは全く誤っていない、ということもあり得る。簡単に言ってしまえば、仕様の方が誤っているのだ。この場合、プログラムの現状に合うように仕様の方を修正する、あるいは仕様自体を再度練り直して再実装する、というアプローチが正解となる。「プログラムに内在する広義の誤り」とは、そういうことも含んでの表現である。仕様が修正されたとしても、練り直して再実装したとしても、いずれも「想定された挙動」が変わることになり、したがって不具合現象も自然と消滅する。
このような状況に対して、「不具合現象を消す」という態度で挑んでいたのでは、正解に辿り着くのはかなり難しい。そもそも整数値0を出力することは、何の解決にもなっていないのだから。
バグとの戦い¶
複雑なプログラム、特にハードウェアが絡むデバイスドライバなどのプログラムでは、本当に一体全体どうしてこう動いちゃうわけ?と頭を抱えたくなるような摩訶不思議な挙動に出くわすことが結構ある。個々の要素については、想定どおりに動いているし、それらを組み合わせたとしても、理論的にこういう風にしかなり得ないはずなのに、そうなってくれない、云々。
このような難問となると、持てる全ての知識と時間と経験とカンを注ぎ込んで戦わなければ、現象の状況把握さえ覚束ない。こういう時は、気分をデバッグ戦闘状態に切り替えて、粛々と次のように行動する。
- 実験を繰り返してデータを採る。摩訶不思議現象とは、実際には何が起きているのかを、可能な限り現実的に把握できるように。
- 実験結果より浮かび上がる摩訶不思議現象の実体を説明できる、原因や遠因を色々と想定してみる。もし、ここでこういうことが起きていたら、そこから論理的に推論して、結果としてこの摩訶不思議現象が起きる、と信じるに足る事象を探す。もし見つかれば、それは摩訶不思議現象を説明する 仮説 となる。
- 上記で得られた仮説を証明できる検証方法を考える。できれば、直接的に証明できる方が良いが、難しければ傍証を積み重ねるという手もある。
- 検証を実行する。ほとんどの場合、これは比較対象実験になるはずである。
- 検証の結果、仮説が棄却された場合は、また2.に戻る。手詰まりを感じたら、1.に戻って状況把握のやり直し。
これまで、多くの摩訶不思議現象に出会い、このような方法で戦いを挑んできたが、一番苦しいのは2.の仮説を立てる段階であると実感している。ここでは、本当に広く深く柔軟な発想が求められる。たとえば、次のようなことを考えるわけである。
- もし、このタイミングでバスのデータ線にノイズが乗って、データがこのように化けたとしたら……
- もし、チップのバグかデータシートの記述ミスが原因で、このタイミングでこのレジスタのこのビットに書き込んでも、値が無視されてしまうとしたら……
- もし、チップがこの信号の入力変化を認識する時間が長く、認識する前に信号が戻ってしまっていたとしたら……
- もし、OSがこちらの想定していないこのタイミングで割り込みをマスクしたりしていたら……
- もし、コンパイラのオプティマイザがコードのこの部分を削ったり、順序を入れ替えてしまっていたら……
続いて苦しいのが3.の検証方法を確立する段階である。特にハードウェアが絡む場合は、検証と言っても一般的にはハードウェアの動作を変更することはできないため、何らかの代替手段を考えたり、傍証を積み重ねるなどの手段を取らねばならないことが多い。
頭は計算機シミュレータ¶
結局、摩訶不思議現象と戦うということは、ハードウェア・ソフトウェアを含めたシステム全体の動作を頭の中でシミュレートする、ということなのだ。シミュレーションのある段階で、微少ではあるが影響のある外乱が生じたとすると、その後システムはどのような挙動を示し、結果としてどうなるか、ということを何度も繰り返しシミュレーションするのである。
そのうち、問題となっている摩訶不思議現象が発生すると考えてもおかしくないケースに思い当たる。これが仮説である。仮説ができると、その検証方法を考案せねばならない。外乱がハードウェアに関係する場合、直接的な検証は難しいことが多い。たとえば、ある特定の条件では、ハードウェアが信号線の変化を読み損なうというような場合は、信号線そのものを観測しても検証はできない。
ここでもまたシミュレーションである。外乱が生じる場合と生じない場合のシステムの挙動をシミュレーションし、問題となっている摩訶不思議現象以外に、観測可能な差異が生じる事象を探すのである。差異が生じる事象が見つかれば、実際に差異が生じているかどうかを実機を使って観測する。差異が観測できれば、仮説が正しいという傍証が得られたことになる。
頭をシミュレータとして動作させるということは、計算機の動作原理を隅々まで知っていなければならないということである。プロセッサの動作のみならず、メインメモリやキャッシュメモリの動作について、外部バスの仕様や動作について、場合によっては回路上の電気信号の流れ方やゲート素子の遅延の状況について、本当に幅広い知識が必要になる。バグと戦うためには、これら計算機の基礎がしっかり身についていなければならない、ということなのである。
デバッグは科学だ¶
さて、上節で示したデバッグの方法論—仮説を立てて検証する—が、近代科学において実践されている「科学的手法」と呼ばれるものとまったく同じであることは一目瞭然である。あらためて論ずるまでもなく、プログラミングという行為は、科学的・工学的な行為である。故に、デバッグという行為も同様に科学的・工学的な行為である。
自分の作ったプログラムが「たまたま」動いているものなのか、「動くべくして」動いているものなのかの判断、あるいは、デバッグによって誤りが確実に訂正されたのか、それともただ単に不具合現象が隠れただけなのかの判断、などなどにおいて、科学的・工学的な思考は絶対に必要なものである。これが正しく身についていないと、正しいプログラムは作れず、正しいデバッグもできないと言って良い。
もちろん、プログラミングには、多くの知識や技巧が必要とされるという側面もある。しかし、筆者としては、このような科学的・工学的思考を持って、プログラミングに接することが最も重要だと考えている。
注釈
- 初稿に由来する表現の差異、ならびにWeb掲載に伴う修正のため、雑誌掲載の内容とは一部文章等が異なることがあります。
- 転載許諾を頂いた株式会社KADOKAWAアスキー・メディアワークスブランドカンパニーならびに旧『UNIX Magazine』編集部の皆様に深く御礼を申し上げます。