借り初めのひみつきち

仮ブログです。

地獄のデバッグ

ある日家にあった Surface 3 で moe を起動してみたら、画面が真っ黒のまま進みませんでした。

現在の moe はウィンドウシステムが起動する前は基本的に何も画面出力しないので何が起こっているのかさっぱりわかりません。
こういう時はどうしたらいいでしょうか?

まずは適当な間隔で画面に進捗がわかるように何かを表示するデバッグコードを配置して実行します。
進捗表示が途中で止まったり期待と違う表示がされたら、その付近に問題があることがわかります。
最初は大雑把に配置し、怪しい関数がわかったらその中に更に配置し、その関数の更に奥に配置し...というのを繰り返してどこで問題が発生しているか突き止めます。

printfデバッグの結果、 SIPI という SMP 初期化時に送信する割り込みに問題があるらしいところまで目星がつきました。
そこで、 SIPI 送信処理をコメントアウトして実行してみるとシングルプロセッサ状態で普通に起動したので SMP の初期化に問題があることは特定できました。

次に、 SIPI の送信方法がおかしいのか、それとも SIPI 受信側に問題があるのか特定するために SIPI 側にデバッグコードを挿入します。
しかし、 SIPI を受け取るハンドラはリアルモードで起動するので、ロングモードで動作する printf 関数はそのままでは動作しませんし、 VRAM はリアルモードからアクセスできないアドレスにあるので何も表示することができません。
そこで、どこまで正常に動くか試すためにまずは SIPI を受け取った直後に無限ループするコードを追加して起動してみます。
moe のスケジューラーは SIPI ハンドラの初期化が終わって割り込みを受け取れるようになったコアにしかスレッドを分配しないので、シングルプロセッサ状態で起動しました。
SIPI を受け取って初期化する処理に問題があることがわかります。

次に、 SIPI ハンドラ内の無限ループの位置をあちこち移動してみます。これを繰り返して起動しなくなった箇所に問題があることがわかります。
最終的に SIPI ハンドラからロングモードに遷移するために EFER MSR に書き込む WRMSR 命令の前後に問題があることがわかりました。

しかし、この WRMSR 命令で EFER を変更しないとロングモードに遷移できません。 WRMSR 命令自体に問題はないように思います。
よくみると EFER に設定する内容がロングモードに遷移するための LME フラグの他に NX ビットを有効にする NXE フラグもセットしています。
NX ビットというのは、ページングに実行禁止属性をつけることで不正な実行を防いでセキュリティを向上させるための仕組みです。
試しに NXE フラグをはずして起動してみると、見事に起動しました。犯人は NXE フラグだったのです!

これを証明するために moe で cpuid の情報が見れるプログラムを実行してみます。
確かに cpuid の NX feature bit は 0 でした。この CPU は NX ビットをサポートしていないということですね。
サポートされていない NXE フラグを EFER に書き込もうとして例外が発生し、ロングモードに遷移前なので例外処理が実装されていなくてそのままトリプルフォルトでシステム停止したということですね。

・・・本当かな?

そもそも同じ型番の Atom を搭載している GPD では普通に NX ビットが使えますし、さいきんの Windows は NX ビットをサポートしていない CPU では起動しないはずです。
ということで、 Windows を起動して cpuid の情報が見れるアプリケーションを実行してみます。

NX サポートしてますね。

意味がわかりません。

釈然としませんでしたが、その日はそれ以上わかりませんでした。

後日、 MSR にこんなフラグがあるという情報をいただきました。

f:id:neriring16:20190316145854p:plain

XD bit というのは NX bit の Intel 方言で、実体は同じものです。
このフラグを操作するコードを追加して起動してみると、 cpuid の NX feature bit がセットされた状態で起動して EFER MSR の NXE フラグをセットしても例外が発生することなく動作するようになりました。

地獄のデバッグを経て一連の問題は全て解決しました。

tl;dr

cpuid の feature bits はモデル固定ではありません。 MSR 等を操作すると実行中に変化することがあります。