2012年02月22日の日記です
経験した中で最大のバグ 2012-02-22 11:30:31 COMP
知人と与太話をしているときに、「自分が経験した、最も恐ろしいバグ」の話になった。
僕がこういうときにする話は決まっている。
プログラムを知らなくても、技術話がわかる人には鉄板でウケる。
生々しい話でもあるし、企業秘密でもある。
ゆえにネット上で公開などはしていなかったのだが、もう時効だと思うので(20年も前の話だ)ちょっと書いてみよう。
---
自分が経験した、と言っても、自分がいた部署の、別チームのプログラムの話。
だから、僕は直接的にはかかわっていない。
まず、「一番恐ろしいバグ」というのは、完成間近のプログラムを一晩エージングテスト(動かし続けて問題が出ないか調べる)しておいて、朝来たら止まっている、というものだ。
画面がブラックアウトしていたら完璧。
どこで、どんな原因で止まったかわからない。
しかも、状況確認の仕組みを備えてもう1度エージングをしたら、…今度はいつまでたっても止まらない。
確実に止まる、ということがわかっているが、その条件がわからず、再現できないのだ。
大抵、エージングテストと言うのは完成間近にならないとやらない。
完成間近なのに、原因不明で、致命的な状況が生じていることだけはわかる。
そして、原因究明がいったいいつになるのかわからない。タイムスケジュールが見通せない。
さぁ、行軍ラッパの音は高らかに鳴った。デスマーチの始まりだ。
---
しかし、これより恐ろしいバグ、と言うのが存在したのだ。
さんざんエージングも行って、停止しないことが確認された。
少なくともそのつもりだった。
開発部隊の手を離れ、品質保証部隊がチェックする。
ここでも、エージングテストや、想像もつかないような操作でバグが出ないかがチェックされる。
それらの多くの関門を潜り抜け、出荷されたと言うのに…市場に出回ってから「止まりました」報告が次々あがってくる。
これは、悪夢以外の何者でもない。
すでに開発機材がつなげられていない機械なので、何が起きているかは、本当に、まったくわからない。
この状況に比べれば、「朝来たらブラックアウト」のほうが、山のように情報があった感じがする。
使用者に平謝り、当面はリセットで凌いでもらうことにして、すぐに原因を究明、修正バージョンを作る、という作業になる。
まずは停止条件の再現をしたいのだが、この作業を「ノーヒント」で行わなくてはならない。
エージングなら散々したし、思いつく限りの過酷な操作もやったはずなのに、これ以上どんな条件があるのか?
とりあえず、最初の行動は頭を抱え込むことだ。
---
そして、これから記すのが、自分の経験で最大のバグ。
使用者に平謝りして、リセットを試みてもらうも…再起動しない!
「当面しのぐ」ことができなくなる。いったんは全商品を回収して、返金に応じなくてはならない。
数千万の金を掛けて開発しているのに、評判はがた落ち、修正して再発売しても、買ってもらえるかわからない。
プログラムは ROM に焼かれているし、いったい「再起動しない」っていうのはどういう状況なんだ?
上司から、原因の徹底究明が言い渡される。
まず、原因は割込みルーチン内にあった。
とあるスイッチが何回押されたか、と言うカウントは、「絶対に」取りこぼしてはいけないことになっている。
そこで、このスイッチは割込み信号を起こすようになっており、カウントは割込みルーチン内で行われる。
割込みルーチン、というのがどういうものか、プログラマでない人にもわかるように解説する必要があるだろう。
通常、サブルーチン(プログラムのひとまとまり)と言うのはプログラムの中から呼び出される。
しかし、外部からの操作に対して、現在実行中のプログラムがどのような状況であっても、必ず処理しないといけないような場合がある。
このときに、「割込み」が発生し、実行中だったプログラムは「ひとまず」停止、サブルーチンが呼び出される。
割込みルーチン内では、実行中だったプログラムが、あとで問題なく作業を続けられるように、「割込み時の状態」を保存することからはじめる。
割込みルーチン内で「破壊」してしまうレジスタ(変数みたいなもの)などを保存し、作業終了後に元に戻してから「割込み終了」するのだ。
この作法を守らないと、プログラムはおかしな動作をすることになる。
--
このとき使用していた CPU は NEC の V60 だった。
この CPU は非常に…信じられないほどの高機能で、「指定アドレスから始まる、指定された終端文字をもった文字列が、別に指定されたアドレスから、指定されたサイズまでのメモリ内の、どこに存在しているか検索する」などの動作が、1命令で完了した。
#と思う。少なくとも文字列操作命令はあったが、記憶によるものだから、少し違うかも。
さて、V60 の文字列操作命令、非常に高機能だが、「暗黙のうちに、2つのレジスタを破壊する」という問題があった。文字列だから、ソース側とディスとネーション側で、2つのポインタが必要になるためだ。
CPU インストラクションには書いてあったが、問題となった割込みルーチンでは、割り込み冒頭の「状態保存」で、暗黙に破壊されるレジスタを保存していなかった。
にもかかわらず、割込み内で文字列操作命令を使用した。
そのため、割込み終了後にプログラムの動作がおかしくなり、「暴走」したのだった。
---
暴走したのはよいとして、なぜリセットまで効かなくなったのか?
このプログラムを動作させていた基盤には、プログラムのデッドコピーを防ぐ仕組みがあった。
この基盤では、プログラムの ROM は、CPU から直接呼び出されるようにはなっていない。
間に、スクランブル回路が入っているのだ。
スクランブル回路は、入ってきたデータに対して一定のビット演算を施し、出力する。
この回路が、データバスに挟まっていた。
スクランブル回路の中には、小さな PROM が入っていた。
PROM とは、一度だけ書き込める ROM だ。ここにキーが書き込まれていて、ビット演算の内容を制御している。
つまり、基盤から ROM を抜き取ってコピーしても、スクランブルが掛かっているので他の基盤で動作させることができない。
スクランブル回路は、一般には市販されないカスタム IC だった上、プログラムごとにキーが変更されるので、動作が推定できない。
しかも、ここからがすごい。
スクランブル回路だけ取り出されて、散々実験されて動作を推定されるのを避けるために、スクランブル回路には「正しい手順でアクセスしないと自爆する」仕組みが付けられていた。
#スクランブルは、状況によって解除したりもできた。
(ROM からのデータ読み込みでスクランブルが解除されるのはよいが、メモリマップド I/O の読み込みでビット演算されてしまうのは余計なお世話)
そのため、スクランブル回路自体にアクセスして、動作を変更できた。
自爆、といっても本当に爆発するわけではない。PROM の内容を破壊してしまうのだ。
こうなると、正しいキーが失われるので、外部からの動作推定は不可能になる。
---
まとめると、つまりはこういうことなのだ。
V60 の命令インストラクションには癖があり、「暗黙の」レジスタ破壊が起こることがある。
しかし、それに気づかなかったプログラマが、割込みルーチン内で破壊されるレジスタを保存しなかった。
該当レジスタは「たまたま」あまり使用されておらず、割込み時の破壊が問題とならなかった。
しかし、絶妙なタイミングで割り込みが起きた場合だけ、割込み後にプログラムがうまく復帰できず、暴走してしまった。
そして、暴走したプログラムは、スクランブル回路に「おかしな方法で」アクセスしてしまった。
スクランブル回路は異常を感じ、自爆。基盤は2度と起動しないようになった…
原因はわかった。
報告を聞いて、原因究明を命じた上司は、責任者を探すように命じた。
責任者は、破壊されるレジスタを保存しないような割込みルーチンを書いた奴だろう。
実際、返金で億単位の損害が出てしまったのだ。誰かが責任を取らなくてはならない。
この割込みルーチンは「絶対に取りこぼしてはいけない」重要信号を扱うものだった。
信頼性が重要なので、はるか以前に書かれたルーチンが使い続けられていた。
(いくつものプログラムで使われていたにもかかわらず、いままで「たまたま」問題が出なかったのだ)
最初に使われたプログラムはどれか?
古いプロジェクトの資料が引っ張り出され、調査されたところ…
プログラムを書いたのは、原因究明を命じた上司だった。
このことが上司に伝えられると、それまでこの件に対して厳しかった上司の態度は一変。
「まぁ、人間だから間違えることもあるよな。ははは…」
…そして、問題は有耶無耶のうちに葬り去られたのでした。
#製品は再発売されたが、悪評がついてしまったためほとんど売れませんでした。
僕がこういうときにする話は決まっている。
プログラムを知らなくても、技術話がわかる人には鉄板でウケる。
生々しい話でもあるし、企業秘密でもある。
ゆえにネット上で公開などはしていなかったのだが、もう時効だと思うので(20年も前の話だ)ちょっと書いてみよう。
---
自分が経験した、と言っても、自分がいた部署の、別チームのプログラムの話。
だから、僕は直接的にはかかわっていない。
まず、「一番恐ろしいバグ」というのは、完成間近のプログラムを一晩エージングテスト(動かし続けて問題が出ないか調べる)しておいて、朝来たら止まっている、というものだ。
画面がブラックアウトしていたら完璧。
どこで、どんな原因で止まったかわからない。
しかも、状況確認の仕組みを備えてもう1度エージングをしたら、…今度はいつまでたっても止まらない。
確実に止まる、ということがわかっているが、その条件がわからず、再現できないのだ。
大抵、エージングテストと言うのは完成間近にならないとやらない。
完成間近なのに、原因不明で、致命的な状況が生じていることだけはわかる。
そして、原因究明がいったいいつになるのかわからない。タイムスケジュールが見通せない。
さぁ、行軍ラッパの音は高らかに鳴った。デスマーチの始まりだ。
---
しかし、これより恐ろしいバグ、と言うのが存在したのだ。
さんざんエージングも行って、停止しないことが確認された。
少なくともそのつもりだった。
開発部隊の手を離れ、品質保証部隊がチェックする。
ここでも、エージングテストや、想像もつかないような操作でバグが出ないかがチェックされる。
それらの多くの関門を潜り抜け、出荷されたと言うのに…市場に出回ってから「止まりました」報告が次々あがってくる。
これは、悪夢以外の何者でもない。
すでに開発機材がつなげられていない機械なので、何が起きているかは、本当に、まったくわからない。
この状況に比べれば、「朝来たらブラックアウト」のほうが、山のように情報があった感じがする。
使用者に平謝り、当面はリセットで凌いでもらうことにして、すぐに原因を究明、修正バージョンを作る、という作業になる。
まずは停止条件の再現をしたいのだが、この作業を「ノーヒント」で行わなくてはならない。
エージングなら散々したし、思いつく限りの過酷な操作もやったはずなのに、これ以上どんな条件があるのか?
とりあえず、最初の行動は頭を抱え込むことだ。
---
そして、これから記すのが、自分の経験で最大のバグ。
使用者に平謝りして、リセットを試みてもらうも…再起動しない!
「当面しのぐ」ことができなくなる。いったんは全商品を回収して、返金に応じなくてはならない。
数千万の金を掛けて開発しているのに、評判はがた落ち、修正して再発売しても、買ってもらえるかわからない。
プログラムは ROM に焼かれているし、いったい「再起動しない」っていうのはどういう状況なんだ?
上司から、原因の徹底究明が言い渡される。
まず、原因は割込みルーチン内にあった。
とあるスイッチが何回押されたか、と言うカウントは、「絶対に」取りこぼしてはいけないことになっている。
そこで、このスイッチは割込み信号を起こすようになっており、カウントは割込みルーチン内で行われる。
割込みルーチン、というのがどういうものか、プログラマでない人にもわかるように解説する必要があるだろう。
通常、サブルーチン(プログラムのひとまとまり)と言うのはプログラムの中から呼び出される。
しかし、外部からの操作に対して、現在実行中のプログラムがどのような状況であっても、必ず処理しないといけないような場合がある。
このときに、「割込み」が発生し、実行中だったプログラムは「ひとまず」停止、サブルーチンが呼び出される。
割込みルーチン内では、実行中だったプログラムが、あとで問題なく作業を続けられるように、「割込み時の状態」を保存することからはじめる。
割込みルーチン内で「破壊」してしまうレジスタ(変数みたいなもの)などを保存し、作業終了後に元に戻してから「割込み終了」するのだ。
この作法を守らないと、プログラムはおかしな動作をすることになる。
--
このとき使用していた CPU は NEC の V60 だった。
この CPU は非常に…信じられないほどの高機能で、「指定アドレスから始まる、指定された終端文字をもった文字列が、別に指定されたアドレスから、指定されたサイズまでのメモリ内の、どこに存在しているか検索する」などの動作が、1命令で完了した。
#と思う。少なくとも文字列操作命令はあったが、記憶によるものだから、少し違うかも。
さて、V60 の文字列操作命令、非常に高機能だが、「暗黙のうちに、2つのレジスタを破壊する」という問題があった。文字列だから、ソース側とディスとネーション側で、2つのポインタが必要になるためだ。
CPU インストラクションには書いてあったが、問題となった割込みルーチンでは、割り込み冒頭の「状態保存」で、暗黙に破壊されるレジスタを保存していなかった。
にもかかわらず、割込み内で文字列操作命令を使用した。
そのため、割込み終了後にプログラムの動作がおかしくなり、「暴走」したのだった。
---
暴走したのはよいとして、なぜリセットまで効かなくなったのか?
このプログラムを動作させていた基盤には、プログラムのデッドコピーを防ぐ仕組みがあった。
この基盤では、プログラムの ROM は、CPU から直接呼び出されるようにはなっていない。
間に、スクランブル回路が入っているのだ。
スクランブル回路は、入ってきたデータに対して一定のビット演算を施し、出力する。
この回路が、データバスに挟まっていた。
スクランブル回路の中には、小さな PROM が入っていた。
PROM とは、一度だけ書き込める ROM だ。ここにキーが書き込まれていて、ビット演算の内容を制御している。
つまり、基盤から ROM を抜き取ってコピーしても、スクランブルが掛かっているので他の基盤で動作させることができない。
スクランブル回路は、一般には市販されないカスタム IC だった上、プログラムごとにキーが変更されるので、動作が推定できない。
しかも、ここからがすごい。
スクランブル回路だけ取り出されて、散々実験されて動作を推定されるのを避けるために、スクランブル回路には「正しい手順でアクセスしないと自爆する」仕組みが付けられていた。
#スクランブルは、状況によって解除したりもできた。
(ROM からのデータ読み込みでスクランブルが解除されるのはよいが、メモリマップド I/O の読み込みでビット演算されてしまうのは余計なお世話)
そのため、スクランブル回路自体にアクセスして、動作を変更できた。
自爆、といっても本当に爆発するわけではない。PROM の内容を破壊してしまうのだ。
こうなると、正しいキーが失われるので、外部からの動作推定は不可能になる。
---
まとめると、つまりはこういうことなのだ。
V60 の命令インストラクションには癖があり、「暗黙の」レジスタ破壊が起こることがある。
しかし、それに気づかなかったプログラマが、割込みルーチン内で破壊されるレジスタを保存しなかった。
該当レジスタは「たまたま」あまり使用されておらず、割込み時の破壊が問題とならなかった。
しかし、絶妙なタイミングで割り込みが起きた場合だけ、割込み後にプログラムがうまく復帰できず、暴走してしまった。
そして、暴走したプログラムは、スクランブル回路に「おかしな方法で」アクセスしてしまった。
スクランブル回路は異常を感じ、自爆。基盤は2度と起動しないようになった…
原因はわかった。
報告を聞いて、原因究明を命じた上司は、責任者を探すように命じた。
責任者は、破壊されるレジスタを保存しないような割込みルーチンを書いた奴だろう。
実際、返金で億単位の損害が出てしまったのだ。誰かが責任を取らなくてはならない。
この割込みルーチンは「絶対に取りこぼしてはいけない」重要信号を扱うものだった。
信頼性が重要なので、はるか以前に書かれたルーチンが使い続けられていた。
(いくつものプログラムで使われていたにもかかわらず、いままで「たまたま」問題が出なかったのだ)
最初に使われたプログラムはどれか?
古いプロジェクトの資料が引っ張り出され、調査されたところ…
プログラムを書いたのは、原因究明を命じた上司だった。
このことが上司に伝えられると、それまでこの件に対して厳しかった上司の態度は一変。
「まぁ、人間だから間違えることもあるよな。ははは…」
…そして、問題は有耶無耶のうちに葬り去られたのでした。
#製品は再発売されたが、悪評がついてしまったためほとんど売れませんでした。