世界を動かす技術を、日本語で。

x86の未定義命令が「ud2」と呼ばれる理由は?なぜ「2」なのか?

概要

  • x86命令 「ud2」は、意図的に 無効な命令例外 を発生させるために使われる
  • コンパイラ は「到達不能コード」などで「ud2」を生成することがある
  • 「ud2」以前には「0F FF」や「0F B9」なども使われていたが、動作が一貫しなかった
  • Intel が公式に「ud2」を導入し、常に例外を発生させることを保証
  • 「ud2」 はパラメータを持たず、安定した例外発生手段として推奨される

x86命令「ud2」とその歴史

  • ud2命令 は、x86アーキテクチャで「無効な命令例外(invalid opcode exception)」を必ず発生させる命令
  • 主な用途 は、到達不能なコード(例:[[noreturn]]関数が戻った場合など)で、実行時にクラッシュさせるため
  • コンパイラ は、実行が到達してはいけない箇所に「ud2」を挿入し、予期せぬ動作を防止
  • 命令名の由来 は、「ud0(0F FF)」や「ud1(0F B9)」と区別するため
    • ud0 :0F FF命令列、かつて無効命令例外を発生させていた
    • ud1 :0F B9命令列、同様に使われていた
    • ud2 :公式に導入された、常に例外を発生させる命令
  • Intel は、「0F FF」や「0F B9」が将来的に定義される可能性や動作の不確実性を解消するため、「ud2」を導入

ud2以前の無効命令の課題

  • ud0(0F FF)ud1(0F B9) は、レジスタやメモリオペランドを持つ命令としてデコードされる
    • 実際にはパラメータは使われないが、デコード時に参照される
  • 命令がページ境界 にまたがる場合、次のページが存在しないと「無効命令例外」ではなく「アクセス違反」になることがある
    • このため、動作が一貫しないリスク
  • 旧世代CPU では、「0F FF」デコード時に即座に例外を発生させるものもあり、挙動に差異

ud2の利点と推奨理由

  • 2バイト命令 でパラメータを持たず、デコードが単純
  • どの環境でも一貫して「無効命令例外」 が発生
  • メモリ参照や追加バイト を必要とせず、ページ境界問題も発生しない
  • 将来的な互換性 や安定動作を重視する場合、必ず「ud2」を使用することが推奨

著者紹介:Raymond

  • Raymond はWindowsの進化に30年以上携わるエンジニア
  • 2003年からWebサイト「The Old New Thing」を運営
  • 同名の書籍(Addison Wesley 2007)も出版
  • Windows Dev Docs のTwitterアカウントでも時折登場し、技術的な逸話を紹介

Hackerたちの意見

x86には詳しくないけど、他のアーキテクチャではソフトウェアの割り込みや例外を発生させることができるんだよね。x86にはこれがないの?それとも、そういう機能が十分なユースケースをカバーしてないのかな?

x86には… INT Ib、INT1、INT3、INTO、BOUNDがあるよ。

基本的には慣習だね。もちろん、割り込みを発生させる代替手段もあるけど、それはアプリケーション特有だったり、指定された命令以外の無効な命令を使ったりすることもあるから、他のプロセッサタイプでは動作が異なるかもしれない。

もしかしたら、コードが無効なオペコードの割り込みハンドラ(INT6)を呼び出したい場合、ハンドラが期待するフラグやレジスタを設定するために追加のコードが必要になるからかも。実際に無効なオペコード例外をトリガーすれば、そういうパラメータは自動的に設定されるしね。

Intel 8086以来、x86には「INT vector_number」という命令があって、これはソフトウェアがISAで指定されたりハードウェア設計者によって実装された多くの種類の例外ハンドラーやハードウェア割り込みハンドラーを直接呼び出すことを可能にする。これらは通常、ソフトウェアの実行やI/Oイベントによって発生する様々な条件があるときに呼び出される。だから、無効な命令例外のハンドラーをINT命令で呼び出せるけど、他の投稿者が言ったように、INT命令だけでは不十分で、例外ハンドラーが期待する情報を含むようにスタックを設定する必要があって、複数の命令が必要になる。このような呼び出しは、無効な命令例外ハンドラーのテストプログラムを書くときには許容されるかもしれないけど、例外を引き起こす値でガードメモリを初期化したいときには受け入れられない。メモリアリアを非実行可能として設定するにはアクセス権が必要だけど、ページ単位でしかできないから、実行可能なコードと非実行可能なデータの両方を含むページには役立たない。もしIntelが永遠に未使用であることが保証された公式なオペコードを定義していなかったら、無効な命令例外を確実に引き起こすための回避策は、ユーザーが無効な命令例外をソフトウェアで呼び出すために256の割り込みベクターの1つを予約することだっただろう。そのベクターには、スタックを正しく設定してから無効な命令ハンドラーにジャンプするシンプルなハンドラーを使えたはず。でも、この回避策には、選んだ割り込みベクターがコンピュータのハードウェア設計者の選択と衝突する可能性があったという欠点があったから、オペレーティングシステムのカーネルや、必要なユーザーアプリケーション(コンパイラなど)の設定可能なパラメータにする必要があっただろう。標準化されていなければならなかったかもしれない。IntelとAMDでオペコードを予約する方が簡単で、標準化や既存のソフトウェアの変更に関する他の要件はなかった。

UD2は、命令の取得(現在のブロックを超えて)とµopsへの変換を停止すると思う。ソフトウェア割り込みやスーパーバイザコールは、多くの場合、最終的に戻って次の命令を実行するから、どちらも行わない方がいいかもしれない。

ソフトウェアの割り込みや例外処理を上げることができるけど、例外処理には、無関係なメモリ領域が初期化されていて、ちゃんと機能する準備ができてる必要があるんだよね。それが自分のコントロール外にあって、OSや組み込みROMに属している場合もあるし、必ずしも自分のケースに対応してるとは限らない。アセンブリやマシンコードはもっと低いレイヤーで動いてるから、「自分が何の大きなものの一部なのかは分からないけど、止まらなきゃいけない」って感じだね。

記事に書いてあるシナリオの中ではint3を使うのが一般的だよね。(到達不可能なコードみたいな)この命令はデバッガーでブレークをトリガーするためによく使われる。

最近はUD0、UD1、UD2がSDMとAPMに載ってるよね。それに、x86-64の64ビットモードで登場した1バイトのバリアント、UDB(D6)もあるし。あと、UDW(FF FF)もずっとあって、これはグループ#5(最初のFF)で、modrmバイトがmod=11b、r/m=111b(/7)、reg=111b(2番目のFF)になってる。これは、全ビットが1のメモリや、デバイスがアクセスを主張しない時に全て1で終端されたバスにとって重要なんだよね。

これは彼の推測に過ぎないの?それとも、何か証拠があるのかな?

この命令が何をするのかはよく知られていて、過去に同じ目的で使われた他の無効な命令の歴史も有名だと思うけど、ちょっと調べても公式なIntelの文書は見つからなかった。彼が誰で、これまでの成果の全体的な質を考えると、単なる推測ではないと信じられるし、まあ、彼の推測を他の人の絶対的な事実よりも信じるよ。

これは、その時の業界で非常に知識があり、尊敬されていた人の一次情報だ。彼が証拠だと言えるね。

Hacker Newsで議論の続きを見る