プログラマーの理想と現実

第8回 再現性

2015.03.04

この記事は、『UNIX Magazine』2004年4月号(2004年3月18日発売)に掲載された同名記事の初稿(著者から編集部に提出したもの)を元に、Web掲載用に一部を修正したものです。10年以上前に執筆したものなので、現在のUNIXを取り巻く環境とは色々と異なることがありますが、プログラミングに対する心構えとしては現在でも通用するものと思い、再掲してみることにしました。

・・・・・・・・・・

前回、メモリ破壊バグを突き止める手法として、メモリの内容が書き潰される箇所を二分探索法により探し出す方法を示した。この方法が適用できる条件として、”メモリ破壊バグの再現性が確立されること”が前提であることに言及した。

この”再現性の確立”という課題、簡単そうに見えるのだが実は相当に手ごわい相手である。今回は現象の再現性について考えてみよう。

再現性の確立とは?

メモリ破壊バグでなくとも、あらゆるバグや動作不良の確認や修正においては、バグや動作不良の現象を何度でもまったく同じように再現できる、という状況を作り出すことが非常に大切である。このことを我々は 再現性の確立 または 再現性の確保 と表現している。

バグや動作不良の再現性が確立できれば、調査対象やポイントを色々と変化させながら、有意な結論が得られるまで何度でも実行を繰り返すことで、バグの原因を追求することができる。デバッグの第一段階は、まずバグ現象の再現性を確保することにある。

決定論的なプログラムの非決論的動作

あるプログラムを実行したところ、バグを踏んでコアを吐いて落ちたとしよう。コアを吐く命令のアドレスやその時のスタックフレームの内容が、プログラムを何度実行してもまったく同じだということであれば、バグ現象の再現性は確立されていると考えてもよい。そもそも、プログラムというものはそれ自身決定論的なものであるから、同じ入力を与えれば同じ挙動を示し、同じ出力が得られるものと考えるのは当然である。

しかし、現実的には多くのプログラムが非決定論的な振る舞いを示すのも事実である。たとえば、入力として整数値を1つ読み込み、それに現在時刻のマイクロ秒下位2桁を加えた整数値を1つ出力するプログラムを作ったとしよう。このプログラムは非常に簡単で、数十行で書ける。一般的なUNIXで実装するならば、現在時刻のマイクロ秒下位2桁を取得するのにgettimeofday()システムコールを用いることになるだろう。

この簡単なプログラムに毎回同じ入力(整数値)を与えて実行すると、実行のたびに異なった出力(整数値)が得られることになる。同じ入力を与えても、毎回異なる出力が得られるのであるから、このプログラムの挙動は非決定論的に見える。

では、このプログラムは非決定論的なのかというと、もちろんそうではない。入力された整数値に現在時刻のマイクロ秒下位2桁を加算して出力する、というアルゴリズムは、極めて決定論的な処理である。決定論的なプログラムが非決定論的な出力を生成する、という怪奇現象を解く鍵は?それは”入力とは何か”というところにある。

入力とは何か?

上記の説明では、”入力は整数値1つ”と書いた。普通に考えれば、確かにこの整数値がプログラムに対する入力である。しかし、これはあくまでもオペレータがプログラムに 与える 入力であって、ロジックとしてのプログラムが 受け取る 入力のすべてというわけではない。

プログラムはgettimeofday()システムコールを呼び出すが、その結果得られる現在日時は、この簡単なプログラムにとっての入力と考えるべきだ、ということである。すなわち、このプログラムに対する入力は、与えられる整数値と gettimeofday()で得られる現在時刻の組でり、現在時刻のマイクロ秒下位2桁はシステムコールを呼び出すたびに異なる値になると想像されるので、結局のところ、入力は毎回異なるということになる。決定論的なプログラムに対して、異なる入力を与えれば、異なる出力が得られるのは当たり前のことだ。

プログラムとはロジックであって、間違いなく決定論的である。よって、同じ入力に対しては必ず同じ出力が得られる。ただし、ここで入力と考えるべきは、ロジックに対しての外的要因すべてである。システムコールの呼び出しはもちろん入力と考えるべきであり、ほとんどのプログラムは、何らかのシステムコールを呼び出して外部とやり取りを行う必要があるため、”同じ入力を与える”ということが難しくなってくる。

また、デバイスからのデータを直接的に扱うプログラムや、ネットワーク経由の通信を扱うプログラム、あるいは複数のプロセスやスレッドが連携して動作するプログラムなどでは、入力の値だけではなく、”いつ入力される”かというタイミングについても考慮する必要がある。

もし、バグ現象の再現性が確立できないのであれば、プログラムに対する入力として考える範囲を広げ、たとえばファイルの読み書きやネットワーク通信の具合、あるいはプロセスの動作タイミングなども含めて検討し、できるだけ同じ入力が同じタイミングで与えられるように工夫する必要がある、ということなのだ。

ジュースも入力!

あるとき、T君が自分の机上端末機のキーボードにジュースをこぼし、キー入力ができない状態になってしまった。T君は復旧を試みようとしたが、その方針について、私に確認のメイルを送ってきた。そのメイルには、以下のようなことが書かれていた。

  • キーボードを解体して洗浄しようと思うが、その方針に問題はないか。(解体すると保証を受けられないなどの問題がある可能性があるため、その確認)。
  • 机上端末機の機種名、管理番号などの情報。
  • こぼしてしまったジュースの銘柄とパッケージ、および概ねの量。

報告に、こぼしてしまったジュースの銘柄と量が含まれているのは、非常に重要なことだ。キーボードが動作しなくなる、というバグ?の再現性を確立する上で、ジュースの銘柄と量は入力としてなくてはならない情報だ。T君のこの素晴らしい報告により、私は必要であればいつでも、キーボードの入力ができなくなるバグ?を再現させ、その原因を解析することができる。

そう、ジュースも入力であることは確かなのだ。

再現性が確立できない場合

一般に、プログラムが複雑になればなるほど、現象の再現性を確立するのは困難になる。これは、ロジックに対する入力として考えるべき事柄が多くなり、そのすべてについて、ロジックに入力されるタイミングを揃えなければならなくなるためである。特に、デバイスドライバなど外部機器との入出力を扱うプログラムでは、外部機器の動作タイミングを制御することが困難であるため、再現性が確立できないケースが多くなる。

再現性が確立できないケースでのデバッグは非常に難しい。特効薬は無く、バグの状況に応じて頭を使って対応するしかない。しかし、再現性が確立できないといっても、ある程度の傾向は掴めるケースもある。たとえば、この入力をこのタイミングで与えると、毎回ではないがバグに当たる確率が高いというケース、あるいはバグを引き起こす入力は作れたが、落ちる箇所や落ち方が毎回異なるというケースなどだ。

このように、再現性が部分的に確立できているようなケースは、入力のタイミングによってプログラムコードの実行される部分や順序が変化することにより、バグを踏んだり踏まなかったりするという状況であると考えられる。デバイスドライバに限ったことではあるが、私の経験からすると、このように再現性の確立が詰めきれない状況は、割り込み禁止区間の設定を誤っているというケースがほとんどである。

割り込み禁止区間の設定を誤ると、割り込みハンドラが想定外のタイミングで実行され、本来アトミックに(不可分で)操作されるべき一連の変数の一貫性が崩れるてしまう。すると、それ以降の一貫性が維持されていることを前提としているコードのいずれかで計算を誤って落ちる。割り込みハンドラが実行されるタイミングは、外部機器の動作に依存しており、デバッグ時に完全に制御できるわけではないため、バグは確率的に発生することになる。加えて、落ちるのは一貫性を崩した箇所ではなく、その後問題となる変数を使って計算を行う箇所であるため、落ちる箇所は毎回異なる可能性がある。

デバイスドライバでなく、普通のプロセスであったとしても、シグナルハンドラを登録して、シグナルマスクを活用しているプログラムでは、同様の問題が起こりうる。再現性の確立が詰めきれないと感じたときは、割り込み禁止、つまりシグナルマスクの設定が正しく箇所で正しく行われているかどうか見直してみると良いだろう。

注釈

  • 初稿に由来する表現の差異、ならびにWeb掲載に伴う修正のため、雑誌掲載の内容とは一部文章等が異なることがあります。
  • 転載許諾を頂いた株式会社KADOKAWAアスキー・メディアワークスブランドカンパニーならびに旧『UNIX Magazine』編集部の皆様に深く御礼を申し上げます。

著者プロフィール

tom

当社設立直後に入社して約30 年、UNIX の移植、日本語化、デバイスドライバ開発、周辺機器ファームウェア開発などに継続的に携わり、現在も現役でUNIX 系OS の移植、改造などの開発業務を行う。社内でもっともプログラムを書いている人の一人。代表取締役社長。

記事一覧Index