プログラミング
x86の未定義命令がud2と呼ばれるのはなぜか? なぜ2なのか?
Why is the x86 undefined instruction called ud2? Why 2? (devblogs.microsoft.com)
要約
x86アーキテクチャにおける未定義命令「ud2」の由来について解説しています。元々は0x0FFFや0x0FB9といったバイトシーケンスが未定義命令として使われていましたが、これらの挙動が将来のプロセッサで変更される可能性や、Hyrumの法則により依存するプログラムが存在したため、Intelは公式にサポートされる未定義命令として「ud2」を導入しました。これは、よりクリーンで予測可能な動作を提供するためです。
全文翻訳
x86コンパイラの出力(あるいは私のように、APIを迂回しようとしたソフトウェアによるクラッシュを見ている場合)で、ud2命令を目にすることがあるかもしれません。それは一体どういうことでしょうか?
ud2命令は、アーキテクチャ的に未定義の命令であり、「無効なオペコード」例外を発生させることが保証されています。一部のコンパイラは、これを「到達不能」コードのマーカーとして生成します。これにより、もし実行が何らかの方法でそこに到達した場合、ランダムな命令を実行するのではなくクラッシュが発生します。例えば、[[noreturn]]とマークされた関数が何らかの理由で返ってしまった場合、コンパイラはその呼び出しの後にud2を配置し、プログラムが次の関数にフォールスルーする代わりにクラッシュするようにします。
いずれにせよ、なぜこの命令は単にudではなくud2と呼ばれるのでしょうか? ud1は存在したのでしょうか? ud1に何がそんなに悪かったので、ud2を作らなければならなかったのでしょうか?
経緯を再構築できると思います。元々、x86にはアーキテクチャ的に未定義の命令はありませんでした。そのため、無効なオペコード例外を強制したい人々は、実行時に確実に無効なオペコード例外を発生させるバイトシーケンスを探しました。誰かが、0x0FFFシーケンスが無効なオペコード例外を引き起こすことを発見しました。ただし、何らかの理由で、命令は内部的に2つのパラメータ(レジスタ宛先とレジスタまたはメモリソース)を取るようにデコードされました。無効なオペコード例外が他の何かが発生する前に発生するため、パラメータは実際には使用されません。
一方、別の誰かが0x0FB9シーケンスも同様の特性を持っていることを発見しました。こうして、0x0FFF信者と0x0FB9支持者の2つの派閥が生まれました。両方のテクニックが機能し、一方が他方の犠牲になるわけではなかったので、彼らの間にはあまり大きな戦いはありませんでした。
Intelは次のプロセッサの開発に着手し、おそらく0x0FFFがもはや無効なオペコード例外を発生させないような変更を加えました。新しい命令が0x0FFFを使用するようにしたのかもしれません。あるいは、依然として未定義でしたが、無効なオペコード命令を発生させる代わりにランダムな操作を実行しただけかもしれません。そして、新しいプロセッサでソフトウェアを実行し始めたところ、一部のプログラムが動作しなくなり、骨の折れる調査の結果、プログラムが0x0FFFを無効なオペコードとして依存していることが判明しました。つまり、彼らはHyrumの法則に遭遇したのです:十分な数のユーザーがいれば、すべての観測可能な動作は誰かによって依存されるようになります。XKCDを義務付けます。
0x0FB9でも同様の発見がありました。人々が信頼性の高い方法で無効なオペコード例外をトリガーしたいと考えるようになった今、Intelの担当者はそれを公式にすることに決め、実際にサポートされ、永久に無効な命令を作成し、それをud2と名付けました。0x0FFFバリアントは遡ってud0と名付けられ、0x0FB9バリアントは遡ってud1と名付けられたため、ud2が推奨される未定義オペコードとなったため、ud2と呼ばれています。ud2の利点の1つは、パラメータなしの2バイト命令であるため、ランダムにデコードされたが未使用のソースと宛先を扱う必要がないことです。
ボーナスチャッター:しかし、ud0とud1の未使用パラメータについてなぜ気にするのでしょうか? ud0とud1も2バイト無効オペコードだと言えませんか? 確かに、3バイト目、あるいはメモリオペランドにオフセットやスケールインデックスがある場合はそれ以上になるかもしれませんが、プロセッサはそれを使用しません。
それは重要です。プロセッサはそれを使用しませんが、デコードはします。そして、命令のデコードがページ内に存在しない部分に及ぶ場合、無効なオペコード例外はまったく発生しません。アクセス違反が発生します。
ボーナスボーナスチャッター:ただし、一部の古いプロセッサは、命令の残りの部分が正しくデコードされるかどうかを確認せずに、0x0FFFをデコードするとすぐに無効なオペコード命令を発生させていました。そのため、0x0FFFがページの末尾にあり、次のページが存在しない場合、無効なオペコード例外が発生することもあり、アクセス違反が発生することもありました。ud2にこだわる方が良いでしょう。その動作は一貫しており、アーキテクチャ的に保証されています。
コンパイラは今日、その仕事をうまくこなしているので、アセンブリについて心配する必要はないと思います。もちろん、Microsoftのコアチームに所属したいのであれば、それは別の話ですが、どれだけのZ世代がアセンブリに精通しているのか疑問です。
心配しますか? いいえ、その必要はありません。しかし、パフォーマンスが重要なコードを書く必要がある場合は、基本的なことを知っておく必要があります。今日の素晴らしいコンパイラでさえ、予測不能なジャンプを含んだり、キャッシュの利用率が悪かったりする、選択の悪いアルゴリズムを必ずしも修正してくれるわけではありません。後者の方が高速でメモリ効率が良い場合でも、リンクリストをベクトルに置き換えてくれるわけではありません。ソースコードが何に変換されるかの一般的なアイデアだけでも持っていれば、コンパイラが最善を尽くすのを助けることができます。私たちのほとんどは、もはやアセンブリコードを手書きする必要はないでしょう。それは認めましょう。それを読むことができるのは、ソースコードが利用できないまれなケースで役立つ可能性があります。また、godbolt.orgで遊ぶときに、コンパイラがあなたのために何をしてくれるかに感謝するのにも役立ちます 😀
UD2とその仲間については聞いたことがありません。プログラムをクラッシュさせたいときは、常に0xCC(INT 3)を見て使用してきました。デバッガが存在する場合にブレークします。UD2の処理は0xCCとどのように比較されますか?
CC(INT 3)はトラップです。主に前提条件/契約違反のシグナルとして使用されます。
UD2は、レイモンドが言ったように「ランダムな命令の実行」は多くのバグを引き起こす可能性があるため、よりハードな停止です。6502のプログラミングを思い出させます。バグがあってプロセッサが未知の領域にジャンプした場合、未使用のRAMがゼロクリアされていた場合、それらのバグをより簡単に見つけられたかもしれません。なぜなら、0x00オペコードはBRK(強制ブレーク)だからです。
x86の場合、UD2命令はパーティーにかなり遅れて登場したので、0x00や0xFFのような「便利な」オペコードはすでに取られていたのでしょうね🙁
Acornマシンでは、BRKベクトルはこのような命令をキャッチし、後続のバイトをエラーメッセージとして読み取りました。(まあ、厳密には、エラーコード、そしてASCIIメッセージです。)これにより、言語やサービスROMは非常に簡単にエラーを報告でき、単一のインターフェースを介して任意のROMまたはアプリケーションからエラーをキャッチすることもできました。したがって、BASICのON ERRORは、BASICだけでなく、ファイルシステム、ネットワーク、接続した新しいハードウェアをサポートするROM、その他何でもエラーをキャッチできました。もちろん、あなたのワードプロセッサ、FORTHプログラム、ゲーム、デバッガソフトウェアなども同様です。もちろん、あなたのコードがランダムな0x00にメモリ内でヒットした場合、次の0x00にヒットするまで画面はガラクタでいっぱいになります。文字出力ルーチンはすべてのグラフィックとテキスト出力を管理していたため、画面をクリアしたり、画面モードを変更したり、文字を再定義したり、プリンタをアクティブにしたり、適切なバイトが読み取られた場合はすべての出力を抑制したりすることもできました。したがって、エラー処理を非常に簡単にしながら、あなたは言うことができます。