- Windows NT 3.1 で
i386kd による Ctrl-C ブレークインをかけると、kd> プロンプトの代わりに対象の 486DX4 システムが再起動し、その原因は enhanced 486 互換性 の問題だった
- NTOSKRNL.EXE の
KiSaveProcessorControlState と KiRestoreProcessorControlState は CPU タイプを検査する際に word 比較 を使っており、CPUID 対応の 486 を Pentium 以上のように誤判定していた
- KPRCB の
CpuType と CpuID がまとめて読み取られることで、CPUID 対応 486 のモデル番号が 256 大きく解釈され、その結果 486 には存在しない CR4 レジスタ へのアクセスが発生する
- 元々搭載されていた 486DX-33 と、SMM のない write-through Am486DX4-NV8T ではカーネルデバッグが正常に動作したため、問題は CPUID 命令を提供する enhanced 486 に絞り込まれた
- NTOSKRNL.EXE 内の 2 か所にある
cmp ds:word_FFDFF138, 5 を byte compare にパッチすれば、オリジナルの NT 3.1 Advanced Server と NT 3.1 SP3 の両方で修正できる
再現環境と症状
- Compaq ProSignia 3080 に Windows NT 3.1 をインストールした後、カーネルデバッグを試みた
- このシステムは実運用で Windows NT 3.1 を実行しており、Windows NT が明示的にターゲットとしていたマシンの 1 つと思われる
- RAM は 128MB に増設し、ソケット式の Intel 486DX-33 を AMD enhanced 486DX4-SV8B に交換した
- この CPU は write-back cache と SMM をサポートし、電圧アダプタソケットに装着されている
- BIOS の 486DX4 対応処理は後回しにして、CPU を 2x 倍率 にジャンパ設定した
- チップセットが L1 write-back をサポートしていなくても、2x 倍率なら Intel 80486DX2-66 とソフトウェア互換になると期待していた
- Intel 80486DX2-66 はこのシステムのサポート対象オプションである
- Windows NT 3.1 のインストール自体は正常に完了した
- Windows NT 3.1 の CD には完全なデバッグシンボルが含まれていたため、カーネルデバッグを試した
- NetDDE がイベントログにエラーを残す理由を調べる目的があった
- 特定の EISA Ethernet カードでシステムがクラッシュする問題もあり、ハードウェア故障の可能性も残っていた
- 最新の Windows 10 開発キットの
kd や ntkd ではなく、Windows NT 3.1 に含まれる i386kd を使う必要があり、これでカーネルデバッグ設定が合う
i386kd で Ctrl-C ブレークインを試みると、対象マシンは kd> プロンプトを表示せず 再起動 する
原因から除外されたもの
- メモリは正常であることを確認済み
- システムファイルの破損ではなかった
- カーネルがデバッグのために停止した際にシステムを再起動させるハードウェアウォッチドッグは有効になっていなかった
- ホスト側の USB-serial アダプタは正常に通信していた
- 偽造 PL2301 らしきデバイスではあったが、デバッガコマンドを誤送信して「reboot system」コマンドを送ったわけではない
- KD プロトコルには実際に reboot system コマンドが存在する
- マザーボードのリモート管理や警告オプションとも無関係だった
実際の原因: enhanced 486 と NT 3.1 カーネルの非互換
- Windows NT 3.1 カーネルは enhanced 486 プロセッサ と互換性がない
- より具体的には、
CPUID 命令を提供する 486 プロセッサで問題が発生する
- カーネルデバッグは次の CPU では正常に動作する
- 元々搭載されていた 486DX-33
- SMM のない、古い non-enhanced コアの write-through Am486DX4-NV8T
- 単に NT 3.1 カーネルデバッグを試すだけなら、Windows NT 3.1 と標準で互換性のある CPU を使うほうが適している
- NT 自体を修正するなら、NTOSKRNL.EXE の CPU タイプ判定バグをパッチする必要がある
問題が発生するカーネルコード経路
- 非互換の直接の原因は
KiSaveProcessorControlState のバグである
- 対応する
KiRestoreProcessorControlState にも同様のバグがある
KiSaveProcessorControlState は NTOSKRNL.EXE 内で 3 か所から呼び出される
- 例外が
KdpTrap を通じてカーネルデバッガに反映されるとき
- Ctrl-C ブレークインでは、タイマ tick interrupt 内のブレークインポーリング機能で breakpoint exception が発生する
KeBugCheckEx が呼び出されるとき、つまり「ブルースクリーン」状態
KiSaveProcessorState が呼び出されるとき
- IDA による NTOSKRNL.EXE の制御フロー解析が完全であれば、この関数は export されておらず NTOSKRNL 内からも呼ばれていないため、実際には発生しないように見える
KiSaveProcessorControlState は拡張 CONTEXT 構造体にプロセッサ制御状態を保存する
- CR0、CR2、CR3 を保存する
- Pentium 以上では CR4 も保存する
- DR0〜DR3、DR6、DR7 のデバッグレジスタを保存する
- GDT アドレス、IDT アドレス、アクティブな TSS selector、LDT selector といったグローバル保護モード設定も保存する
KPRCB フィールド解釈エラー
- プロセッサタイプ判定には KPRCB の値が使われる
- KPRCB は KPCR の一部である
- ブートプロセッサまたは単一プロセッサシステムの KPRCB は仮想アドレス
FFDFF120 にあり、このメソッドではハードコードされている
- Geoff Chappell による NT 3.1 の KPRCB 関連フィールドは次の通り
+018 CHAR CpuType
+019 CHAR CpuID
+01A UShort CpuStep
- これらのフィールドは
KiSetProcessorType で初期化される
- 486 プロセッサでは offset 18 byte は 4 に設定される
- Pentium プロセッサでは 5 に設定される
- Pentium Pro および Pentium II/III プロセッサでは 6 に設定される
- offset 19 byte は、プロセッサが CPUID をサポートし、かつ「reasonable」に動作するかどうかを示す boolean フラグである
- 問題の比較命令は
FFDFF138 アドレスの byte ではなく word を読み取っている
FFDFF138 は KPRCB 先頭から 18h byte 離れた位置である
- そのため
CpuType だけでなく、直後の byte である CpuID までモデル番号の一部として一緒に解釈される
- CPUID をサポートするプロセッサでは、モデル番号が実際より 256 大きく扱われる
- CPUID 対応の 80-4-86 は、Windows NT 3.1 では 80-260-86 のように扱われる
- 260 は Pentium の基準である 5 よりはるかに大きいため、カーネルはそのプロセッサに CR4 があるはずだと判断 する
- enhanced 486 には CR4 がないため、ブレークイン経路で問題が発生する
パッチ方法
- バグを確認できれば、修正は単純である
- NTOSKRNL.EXE 内で
cmp ds:word_FFDFF138, 5 命令は 2 回しか現れない
KiSaveProcessorControlState
KiRestoreProcessorControlState
- どちらの箇所も word compare を byte compare に変更する必要がある
- 16 進エディタで次のバイト列を 2 回パッチする
- 変更前:
66 83 3D 38 F1 DF FF 05
- 変更後:
90 80 3D 38 F1 DF FF 05
- この修正は、オリジナルの NT 3.1 Advanced Server 配布版の NTOSKRNL.EXE と NT 3.1 SP3 の両方に適用できる
1件のコメント
Hacker Newsのコメント
昔、サービスが生きているか確認するためにポートチェックを作ったら、マシン上のTCPポートをいくつか開くだけで会社の半分を落としてしまったことがある
めちゃくちゃな時代だった
本当に遠くまで来たものだ
https://en.wikipedia.org/wiki/Ping_of_death
一部のマシンには、何年もパッチが当たっていないSMB脆弱性もあった。すでにMetasploitがあった時代なので、コマンドをいくつか打つだけでローカルネットワーク上のWindowsホストの大半にVNCを注入できた。最近は少なくともパッチ適用の速度はものすごく速い
ああ、レトロコンピューティングの沼。現実世界への影響からは無害に切り離されているけれど、それでも本当に満足感がある
ギーク向けの本物のハニーポットだ。結局「コメントをさらに6件表示」の展開ボタンを押してしまった
今日は少し仕事ができるかもしれない
でも486となると? ブルースクリーンや、メモリが足りなくてディスクスワップに延々待たされた記憶が多すぎる。まだ自分には早すぎるようだ
「解決策は明らかだ」は本当にその通り
おそらくトラップコードの逆アセンブルをひたすら読み続けて、問題を見つけたのだろう。QEMUや他のデバッグ手段を使えたとは思えない
末尾の隠れたコメントの中に埋もれている方法まで必ず読むべき
最低得票のコメントを隠すわけでもなく、新しい順や古い順を隠すわけでもない。ランダムに感じるし、不要だ
スペースが問題なら、隠すよりページ分割を入れたほうがいいと思う
昔のAbit、Asus、もしかするとMSIのボードで互換性のために使っていたslotketアダプタのようなものには、本当にオーバークロック能力があった
RAM速度はいくつか、マザーボードが倍率設定でどの程度のクロックまで耐えられるかが鍵だった。CeleronとPentium IIがあった、比較的初期のオーバークロック時代だった
ある時期、互換性の都合でCeleron 600をアダプタに挿し、さらにSlot II to Socket 370アダプタに挿して、[1]のような構成で1.2GHzで問題なく動かしていた。Windows MEでは1.4GHzまで上げていた気もするし、最終的にそのCPUを焼いてしまった
[1]http://krick.3feetunder.com/370mod/
質問についた回答が本当に素晴らしかった
テーマも良く、回答もとても良かった
質問者が自分で回答まで書いているのがいい
こういうのは好きだ。人々がレトロコンピューティングに関心を持ってくれて本当にうれしい
こうした問題と解決策を、みんなのために文書化するのは良いことだ
このCPUIDというものが良くないアイデアだとは思っていた
なぜ質問と回答のタイムスタンプが同じだったのだろう?
おそらく、この情報をSOのQ&A形式で投稿したほうが、誰にも見つけられないブログ記事より長く残ると判断したのだろう
開かなくても何の話か分かったのは悪いことだろうか?
まあいい、最近の若者にうちの芝生から出ていけと怒鳴りに行かなければ
頼れるのは機転と、必要なら固定電話で呼べる達人たちくらいだった。有用性がまちまちな回答でいっぱいの素晴らしいインターネットなんてなかった