「ポケットモンスター 赤・緑」では有名なセレクトBBバグというものがあります。どうぐの選択画面で7番目の道具を選択した状態でキャンセルし、てもちポケモンの画面でも再度セレクトボタンを押すと、バグポケモンと入れ替わるというバグです。
えんじにゃー的観点に従って、このバグが現代のプログラミングにおいても発生するのか、このようなバグを避けるにはどうしたらいいのかという点を考察してみます。
時代背景
当時のプログラミングはアセンブリやC言語といった、低級言語で行われることが通常でした。現在のようなガーベジコレクションといった仕組みはなく、またRustのような所有権システムもないようななかで、自己の責任においてメモリを管理する必要がありました。この点は、このセレクトBBバグが起きた背景として重要です。
セレクトBBバグが行われる仕組み
ポケモンやわざ、道具をセレクトボタンで選択すると、そのポケモンのわざや道具が選択状態になるということは周知のとおりです。そして、別のわざや道具と入れ替えることができるようになっています。
このとき、「今どのポケモンやわざ、道具を選択しているか」という選択のインデックスを、単一の値で管理していました。
当時、非常に少ないメモリ空間をやりくりしないといけなかったことを考えれば、わざ・ポケモン・どうぐの並び替えは同時に発生しなかったので、共通の値を用いることは合理的だったといえます。
そして、ポケモンやわざを選択してキャンセルしたときは、正しくそのメモリの値の初期化が行われていました。しかしながら、道具のときは、このキャンセルのときの初期化処理(ゼロ埋め)が行われていませんでした。
このため、どうぐの5番目を選択した状態でわざを表示すると、本来存在しない5番目のどうぐを選択した状態になってしまいます。道具の7番目を選択した状態でポケモンを表示すると、本来存在しない7番目のポケモンを選択した状態になってしまいます。この本来存在しないわざやポケモンと「入れ替え」の操作を行うと、周知の通りおかしなわざと入れ替わったり、いわゆるバグポケモンと入れ替わってしまいます。
これが起きるメカニズムについては、ポケモン赤緑 7番目の道具で「レベル100」になる理由という動画が非常にわかりやすいのでおすすめです。
直接の原因は?
直接の原因は、上記の動画でも触れられているとおり、特定のメモリの値のクリア忘れです。わざやポケモンは、その値を使ったらクリアする処理が適切に実装されていました。しかしどうぐをキャンセルするときだけその処理を忘れていたため発生したのです。
現在のプログラミングでは直接メモリを扱うことは少ないですが、C++などで初期化されていない変数をそのまま使用すると似たような不具合を発生させることはあり得ます。しかしながら、多くの高級言語では、使用したメモリの初期化を忘れたため、あるいはメモリの値のクリアを忘れたために、他の画面で意図しない動作を引き起こすことはそうめったにないでしょう。
もっと根本の原因は?
表面的には、メモリ(変数)の値のクリア忘れによって発生しましたが、もうちょっと追求すると、もっとさまざまな要因が考えられます。
不正なメモリの読み取り
わざのデータを示さない「5番目のわざ」やポケモンのデータを示さない「7番目のポケモン」を、そのままわざやポケモンと解釈してしまったことも原因のひとつです。
現在のプログラミング言語の多くでは、このように本来の配列の個数を超える添字にアクセスすると、IndexOutOfBoundsException
などが発生したり、undefined
やnull
を取得することになります。
全く無関係の値を無理やりあてはめて評価して動いてしまうことはないにせよ、配列の添字を超えたアクセスを起こしうるというのは現代の高級言語によるプログラミング言語でも起こりえます。最悪の場合、例外がキャッチされずクラッシュするでしょう。あるいは、JavaScriptであれば画面にundefinedという文字がそのまま表示されていたかもしれません。
もしも赤・緑が現代的なNull安全な言語で実装されていたなら、nullの場合として早期リターンして、何も起こらない現象が起きていたかもしれません。(それはそれとして、選択中のカーソルがおかしい状況は発生していたかもしれません。)
単一の変数を複数の目的で使い回す
当時、非常に少ないメモリアドレス空間をやりくりするために同じメモリアドレスを複数の目的で使用していました。
しかしながら令和になった現代で、このような手法を取ることは極めて稀です。デジタルサイネージですらWindowsやAndroidを搭載しているこのご時世に、同じ変数を複数の目的で使用するメリットは、(電子工作やマイコン制御などでない限り)ありません。
単一のグローバル変数であればそうかもしれません。しかしながら、シングルトンパターンなどで間接的にアクセスされる場合はどうでしょうか?あるいは、DIによって管理されたオブジェクトが同じインスタンスを指している場合どうでしょうか?そして、それらが異なる目的で複数の画面で使用されようとしていた場合どうでしょうか?
そのように考えれば、意図せずに同じ値を参照して、多目的に使用されるバグは現在でも引き起こし得ます。
リソースのdispose忘れ
使用した値を自己の責任でもう使わないと宣言しなければいけないケースは、メモリ直接でなくてもいくつかあります。特に、ファイルの入出力や画像処理、DB接続などでIDisposable (C#)やClosable(Java)を継承したクラスを扱うときです。あるいは、画面のContextを参照するクラスを扱うときもそうですし、ReactにおけるuseEffectのクリーンアップ関数もそうです。
これらの処理は、例外発生時を含めて常にクローズ処理やアクセスの処理を適切に行う必要があります。そうしないとメモリリークを引き起こしたり、タイマー関数が永遠に呼ばれ続けたり、専有したポートやファイルを解放しないままになってしまったりするという実害が発生します。
これを避けるため、C#にはusing、Javaではtry-with-resource文があります。あるいは、flutter_hooksでは、TextEditingControllerなどのdispose忘れを避けるための仕組みが存在します。
もしも、このような特定のメモリアドレスの専有の仕組みが言語仕様に存在したり、静的解析によって検出できた場合、セレクトBBは起きなかったかもしれません(当時の環境では無理です)。
カプセル化という概念がなかった
現代的なプログラミングにおいては、「何を公開して」「何を公開しないか」は重要な概念です。不必要に内部の実装を外部は知る必要がないようにして、プログラムが意図しない干渉を起こさないようにする必要があります。
この当時のプログラミングでは、そのような概念はありませんでした。現在のプログラミングの感覚からすると、わざの選択画面はポケモンの選択画面の状態に干渉すべきではありませんし、どうぐとわざも同様です。当時は、そもそもメモリに直接アクセスするプログラムを書いていたわけですから、すべての変数がグローバル変数という状況ともいえたわけです。
現在ではそのような極端な状況下でのプログラムを記述することは、少なくとも一般のシステム開発やアプリ開発ではないでしょう。しかしながら、getterは誰がアクセスしてよくて、setterは誰がアクセスしてよくてといった公開範囲を厳密に定義してカプセル化を行わないと、意図しない箇所で値が変更されるリスクが存在します。そしてそのリスクはセレクトBBバグのようなことを引き起こし得るかもしれません。
検出が難しいバグ
「どうぐの7番目を選択した状態でキャンセルを行い、ポケモンの画面を開き、即座に入れ替える操作を行う」という手順は、ホワイトボックステストでは極めて検出が難しいですし、ブラックボックステストでも「同じ共有のメモリアドレスを使用している選択状態が、初期化し忘れている可能性がある」という観点まで気を利かせるのは難しかったようです。実際難しかったため、このバグは検出されずに出荷され、その後はご存知の通りです。
そして当時ではアップデートを配信して修正するといったことも難しかったです。
現在ではどうでしょうか、それはポケモンの歴史がまた語っています。「ポケットモンスター ダイヤモンド・パール」では、いわゆる四天王なみのりバグによりゲームの進行が不可能になる現象が発生しました。トイザらスなどの店舗に配置されたNintendo Stationで、修正パッチが適用できるようにしたことは、覚えている人も多いのではないでしょうか。当時インターネット回線は普及しつつあったものの、PCと接続する必要があるなど、必ずしも多くの人が利用可能な環境ではありませんでした。
もう少し時代をくだると、「ポケットモンスター X・Y」はミアレシティでレポートを書くとまれにゲームが進行不能になるバグがあったことを覚えている人がいるかもしれません。この当時では既にインターネットがかなり普及していたため、インターネット経由でアップデートを行った人も多いのではないでしょうか。
一方で現代ではインターネットが普及しているため、アップデートを簡単に配信できるようになったため、初回出荷時の品質は妥協してもよいかといえば、「ポケットモンスター ブリリアントダイヤモンド・シャイニングパール」がそれを物語っているといえば伝わるのではないでしょうか。
現代でも検出可能なバグだったか?
話が横道に逸れましたが、セレクトBBバグについて、現在のプログラミングでこのバグが検出できるかどうかについて考察してみます。
このセレクトBBバグは、一種のメモリリークのバグとして捉えることができます。メモリリークを発生させているかどうかを検出することは、現在のプログラミングでも難しいです。
ホワイトボックス的にテストを設計すれば、たとえばどうぐのメニューの状態がシングルトンであると分かっている場合には、このクラスを2回呼び出して2回目にはクリアされていることをテストする、といった単体テストの観点のテストは実施することができます。しかしながら、ここまで気を利かせた単体テストの設計はやはり難しいかもしれません。
テストカバレッジを管理しているならば、統合テストでdispose(リソースのクリア)の処理が呼ばれていないことで、このリソース解放忘れに気付くことができるかもしれません。ユニットテストでは、テスト自身がdisposeを実行するため気づくことは難しいでしょう。
いろいろ考えましたが、現代的な手法でも統合テストでのテストカバレッジ計測で気づくのが最も現実的な手段のように思います。シンプルなバグでありながら、リソース解放の不具合は現在でも非常に厄介であることがわかります。
リソース解放やメモリリークのバグとして捉えればそうですが、一方でそもそもどうぐのメニューをシングルトンにする設計が誤っています。毎回初期状態に戻っているべきで、毎回閉じたときにすべての状態がクリアされているべきであるので、このクラスは毎回作り直される必要があります。この場合、この解放は(参照をどこかのクラスが持ち続けているといったメモリリークを引き起こさない限り)コンパイラやインタプリタ、あるいは実行環境の責務です。こうして考えれば、そもそも起こし得ないバグともいえます。一方でDIなどを使用する場合は、そのクラスのスコープが適切かをどこかでテストしなければならないかもしれません。
現代のプログラミングでも起こし得るバグか?
これは、避けられる側面とそうでない側面があると考えられます。
- メモリ管理による不具合と捉えれば、このようなメモリリソースの管理は高級言語では、コンパイラやインタプリタの役割のため、通常起きることはありません。
- 不正な配列添字の読み取りの不具合と捉えれば、これは言語仕様やそれをどう使用するかによります。
- 通常、高級言語においてまったく無関係のメモリ値を強引に何かの型にあてはめて解釈するといったことは起こりえません。
- しかしながら、それがnullやらnilやらundefinedのアクセスとなるか、例外のスローとなるか、またその値は安全に読み取られるかは言語仕様やライブラリ仕様によります。
- 例えば、Javaであっても、StreamAPIを用いてOptionalの型で取得し、誤った添字の値(null)だった場合の処理を考慮すれば防げるかもしれません(めんどくさいですが)し、あるいは単純に添字でアクセスすると例外をスローするかもしれません。
- Kotlinであれば、getOrNull関数を使うだけで避けられるでしょう。
- 状態管理不正の不具合と捉えれば、このような不具合は現在でも起こしえます。フラグ管理や選択インデックスが誤った値になる可能性は、実装を誤れば十分にあり得ます。
- スコープ管理の不具合と捉えれば、単なるグローバル変数の乱用やシングルトンパターンの不適切な利用を行えば、これはやはり起こりえます。何を外部からアクセスできるようにして、何はアクセスできないようにするかを適切に設計しないと、意図しない箇所で意図しない書き込みを行わせてしまうリスクがあります。
おわりに
このようにセレクトBBバグは、非常に古い時期のコンピュータによるバグでありながら、意外にもそれは現在にも多岐に通ずる重要なバグだったのです。単純なメモリの値のクリアのし忘れという点にとどまらず、状態・リソース・スコープ管理や言語仕様・ライブラリによる責務など、さまざまな観点から評価することで、このバグの奥深さが理解できたことでしょう。
今回はセレクトBBバグを技術的に現在の環境でも起こしうるか、避けるためにはどうしたらいいかについて考察しました。途中で挙げた「なぞのばしょ(四天王なみのり)」バグも、メカニズムとしてはシンプルながら、その社会的影響や企業としての損失、あるいは実際のプレイヤーの評価といった側面を考察すると面白いかもしれません。