2011-01-13

Old New Thing: 変な子のNOP

My, what strange NOPs you have! - The Old New Thing - Site Home - MSDN Blogs

僕のオフィスを掃除していたら、古いドキュメントを見つけた。Windows 95には、奇妙なNOP命令がそこらじゅうに使われていたのだ。

1987年以前に製造された80386を、B1 steppingという。この初期型の80386には、Windowsに問題を与えるバグがいくつがあったのだ。例えば、文字列命令(movs等)に続く命令が、別のサイズのアドレス(訳注:16bitなら32bit、32bitなら16bit)を使っている場合や(例えば、movs es:[edi], ds:[esi]に続いて、mov ax, [bx])や、続く命令が別のサイズのアドレスのスタックを使っている場合(例えば、 movs es:[edi], ds:[esi]を16bitスタックで行い、次に命令がpushの場合)、movs命令が、正しく動かない。このような細かい、「もし星の配置が正しければ」というようなCPUのバグは、結構ある。

ほとんどのCPUバグは、32bitコードと16bitコードを併用したときにしか発現しない。したがって、純粋な16bitコードや32bitコードならば、この問題に遭遇することはまずない。Windows 3.1は、16bitコードと32bitコードの併用はほとんどなかったので(ユーザーモードはすべて16bitで、カーネルモードはすべて32bit)、Windows 3.1では、この問題に遭遇することもなかった。

Windows 95は、しかし、ユーザーの16bit世界から32bit世界への移行のために作られたOSなので、かなり多くの併用が行われている。結果として、問題のあるコードシーケンスが使われることは、まれではなかった。

古いCPUをサポートするか、切り捨てるかという決断をしなければならなかった。市場を調査した結果、当時、それなりに多くのコンピューターが、80386を搭載しており、サポートする意義はあるように思われた。

アセンブリ言語でコーディングする開発者には全員、B1 steppingで問題が起こるコードシーケンスが伝えられた。そのようなコードシーケンスを発生させないようし、また、既存の問題のあるコードを見直すためである。手動での検証を助けるため、僕はWindows 95の全バイナリーに対して、問題のあるコードシーケンスが含まれているかどうか調べるプログラムを書いた。ツールでシーケンスが見つかるたびに、該当部分を調査し、問題があれば、その部分を担当する開発者に知らせた。

ほとんどのケースでは、問題のあるコードは、単にNOP命令の挿入で修正できた。問題が、「Xという命令に続くYという命令」であれば、二つの命令の間に、NOPを挿入して、「お開きにし」、問題を回避させるのだ。ただし、通常のNOP命令自体が、種類Yである場合があるので、Yにならないような特別なNOPを使う必要があった。

例えば、以下は色数フォーマットの変換を行う関数である。

push    si          ; borrow si temporarily

        ; build second 4 pixels
        movzx   si, bl
        mov     ax, redTable[si]
        movzx   si, cl
        or      ax, blueTable[si]
        movzx   si, dl
        or      ax, greenTable[si]

        shl     eax, 16     ; move pixels to high word

        ; build first 4 pixels
        movzx   si, bh
        mov     ax, redTable[si]
        movzx   si, ch
        or      ax, blueTable[si]
        movzx   si, dh
        or      ax, greenTable[si]

        pop     si

        stosd   es:[edi]    ; store 8 pixels
        db      67h, 90h    ; 32-bit NOP fixes stos (B1 stepping)

        dec     wXE

注意すべきこととして、昔のNOPは使えなかった。32bitアドレスのオーバーライドプレフィクス付きのNOPを使う必要があった。そう、これは通常のNOPではなく、32bit NOPなのである。

B1 stepping対策として、C言語を書く開発者は、比較的、苦労はしなかったが、楽でもなかった。苦労しなかったというのは、コンパイラーがコード生成を行うので、それに関しては特に心配する必要はなかったのである。楽でもなかったというのは、コンパイラーの開発者に、問題のあるコードシーケンスを生成してしまうCコードを、わざわざ教えてもらわなければならなかったのである。(For example, there was one bug that manifested itself in incorrect instruction decoding if a conditional branch instruction had just the right sequence of taken/not-taken history, and the branch instruction was followed immediately by a selector load, and one of the first two instructions at the destination of the branch was itself a jump, call, or return. The easy workaround: Insert a NOP between the branch and the selector load.)(訳注:taken historyのまともな日本語訳を思いつかなかったため断念)

また、B1 steppingnoのいくつかのバグは、簡単に避けることができた。例えば、B1 steppingは、最初の64KB分のメモリーに対しては、仮想メモリーをサポートしていなかった。まあ、そのアドレスでは仮想メモリーを使わなければいいだけの話だ。仮想メモリーが有効化され、ハードウェアのプリフェッチで、特定の状態になり、0x800000F8から0x800000FFの範囲のアドレスにアクセスする浮動小数点小プロセッサー命令が実行された場合、CPUは、0x000000F8から0x0000000FFの範囲のアドレスを読み込んでしまう。これも、簡単に回避できる。0x80000xxxにメモリーを割り当てなければいいのだ。2GB境界近くのアドレスは、触らずにそっとしておいたほうがいい理由がまたひとつできたわけだ。

私はたまたま、B1 steppingを搭載した古いコンピューターを、オフィスに持っていた。処理速度は遅かったが、ちゃんと動いた。たしか、テストチームの人が、Windows 95がB1 stepping CPU上でちゃんと動作するかどうかを検証するために、「接収」していった記憶がある。

製品開発の後期になって(最後のベータの後)、上層部は先の判断を翻し、B1チップをサポートしないことに決めた。たぶん、テスターがB1 stepping関連のバグを発見しすぎていたのかもしれない。たぶん、すべてのソースコードを検証し、さらに、B1問題をすべての開発者に教育するようなコストが、販売対象の顧客を少し減らす事による損益を上回ったのかもしれない。どういう理由にせよ、B1 steppingサポートは撤回され、古いチップを使っている顧客は、Windows 95のインストール時に、エラーが表示されるようになった。サポート部門の人の仕事を少し楽にするため、エラーメッセージ中のエラーコードは、Error B1だった。

当時、苦労したんだろうなぁ。

No comments: