ud2 が「2」なのは、先に ud0 と ud1 があったからx86 の命令列を見ていて ud2 に出会うと、ちょっと不思議に感じる。どちらかというと「未定義命令」という雑な名前で十分に見えるのに、わざわざ 2 が付いているからだ。Raymond Chen の短い記事は、その素朴な疑問に対して、Intel の仕様と現場の歴史がどう重なって今の形になったのかをたどっている。単なる命名の話に見えるが、実際には「何を壊してよいのか」「どの挙動を仕様として保証するのか」という、CPU 設計のかなり大事な話でもある。
ud2 は「到達してはいけない場所」を確実に落とすために置かれている記事の出発点は、x86 のコンパイラ出力や、API を横取りするようなソフトウェアのクラッシュ解析で ud2 を見かけることだ。ud2 は architecturally undefined instruction、つまりアーキテクチャ上「未定義」とされる命令で、実行されると必ず “invalid opcode” 例外を起こす。コンパイラはこれを、そこに来ては困る場所の目印として使う。たとえば [[noreturn]] が付いた関数が、理屈の上では戻らないはずなのに戻ってきてしまった場合、その直後に ud2 を置いておけば、次の関数へ勝手に落ちていくより先にクラッシュしてくれる。
では、なぜ ud2 なのか。なぜ 2 なのか。記事の仮説はこうだ。最初の x86 には、「この命令を実行したら必ず invalid opcode になる」という正式な仕組みはなかった。そこで人々は、偶然そういう例外を起こすバイト列を探し始める。ひとつは 0F FF だった。これは内部的には、レジスタを目的地とし、レジスタかメモリを स्रोतに取る、2 つのオペランドを持つ命令のようにデコードされるらしい。ただし、実際にはその意味を使う前に invalid opcode 例外が出るので、オペランドは事実上使われない。
別の陣営は 0F B9 を見つけた。これも同じように invalid opcode を起こせる。こうして 0F FF 派と 0F B9 派が並立するが、どちらも機能したので、争いはそれほど激しくなかった。ところが Intel が次のプロセッサを作るとき、0F FF のふるまいが変わったらしい。新しい命令に使われるか、あるいは単に昔と同じ invalid opcode ではなくなったのかもしれない。すると、あるソフトウェアが動かなくなり、調べてみると 0F FF が invalid opcode であることを前提にしていた。これは Hyrum’s Law の典型だ、と記事は言う。観測可能な振る舞いは、十分に利用者が増えると誰かに依存されてしまう、という話だ。同様のことが 0F B9 にも起きた。
そこで Intel は、頼れる「永久に無効な」命令を正式に用意し、それを ud2 と名付けた。名前の理由は単純で、0F FF の系統が後から ud0 とされ、0F B9 の系統が ud1 になったため、その次が ud2 になったという説明だ。ud2 の利点は、2 バイトの命令で、余計なオペランドがないことにある。ud0 や ud1 では、実際には使わないとはいえ、内部では命令のデコードが進み、その途中で次のページまで読み込もうとすることがある。その場合、invalid opcode ではなく access violation になることがある。さらに古い CPU では、0F FF を見ただけで先に invalid opcode を出すものもあって、ページ末尾に置いたときの挙動が機種によってぶれた。だからこそ、仕様として保証された ud2 を使うのが安全だ、というのが記事の結論だ。
この話で面白いのは、ud2 が単なる便利な罠ではなく、CPU の歴史の継ぎ足し方そのものをよく表している点だと思う。最初は正式な機能がなく、現場が「たまたま落ちる」バイト列を使い始める。次にそれが広まると、Intel はもはや無視できなくなる。仕様に書かれていなかった挙動でも、ユーザーがそれに依存してしまえば、実質的な互換性要件になるからだ。ud2 は、そうした野良の慣習を後追いで正式化した結果に見える。きれいに設計された命令というより、歴史の堆積物だ。
しかも ud0 と ud1 が「後から」名付けられているのが味わい深い。普通なら未定義命令に番号を振る発想自体が変なのに、x86 ではそれをやってしまう。ここには、完全な整理よりも既存ソフトの動作を優先する文化が透けて見える。新しい設計論としてはあまり美しくないかもしれないが、互換性を守る世界ではむしろ自然だ。昔のソフトが勝手に頼ってしまったなら、それを壊さず、なおかつ今後の開発者にはより安全な代替を用意する。ud2 はその妥協点だと思う。
ud2 の価値は「落ちる」ことではなく、「どこで落ちるかが決まっている」ことにある開発者の目線で見ると、ud2 の大事さは「クラッシュする」こと自体ではない。クラッシュのさせ方が、予測できるところにある。未定義動作をそのまま流してしまうと、次の命令が何だったか、レジスタがどう壊れたか、メモリに触れたかどうかで、症状がまるで変わる。結果として、再現しにくい不具合や、たまたま静かに壊れるバグになる。ud2 はそこを断ち切って、ここで止まるべきだと CPU に言わせる。デバッグのしやすさというより、被害の広がりを止める道具に近い。
記事が最後に触れているページ境界の話も重要だ。ud0 や ud1 は、オペランドのデコードの途中で別ページを読み、そこで access violation を起こす可能性がある。つまり「必ず invalid opcode を出す」つもりで置いたのに、実際にはページアクセスの失敗として別の例外になることがある。これは現場ではかなり厄介だと思う。クラッシュの種類が揺れると、ログの見え方も変わり、原因特定が面倒になる。ud2 が推されるのは、単に新しいからではなく、こうした曖昧さを減らせるからだ。
ud2 は、日常的に手で打つ命令ではない。ふつうのアプリ開発者が意識する場面は少ないし、コンパイラがよしなに挿してくれることも多い。ただ、クラッシュダンプを読む人、最適化されたコードを追う人、あるいは noreturn のような制御フローの保証を真面目に扱う人にとっては、かなり実用的な存在だと思う。とくに「本来ありえない経路に入った」ときに、曖昧な続きを許さず、その場で停止させる役目は大きい。
一方で、この話は x86 の互換性設計の難しさもよく示している。仕様を変えないようにしているつもりでも、実際には既に誰かが依存している。そこで Intel は、昔の挙動をそのまま温存するのではなく、より安全で一貫した新しい逃げ道を正式化した。私はそこに、後付けでもなお工学として筋を通そうとする姿勢を見る。美しい命名とは言いにくいが、実務ではこういう地味な整備のほうがずっと効く。
参考: Why is the x86 undefined instruction called ud2? Why 2? - The Old New Thing