借り初めのひみつきち

仮ブログです。

最小ステップで作る UEFI OS v0.3.1

前回はコンテキストスイッチを実装しましたが FPU(SSE) のコンテキストスイッチには未対応だったので実装します。

Lazy FPU Context Switch

FPU や SSE はレジスタが多く汎用レジスタに比べてコンテキストスイッチのコストが高いわりにほとんどのプログラムではこれらの機能をほとんど使用しません。
ほとんど使われていない大量のデータを毎回メモリに出し入れするのは壮大な無駄なので Lazy FPU Context Switch という手法がよく使われます。

これはコンテキストスイッチの時点では FPU の状態はそのままにしておいて、はじめて FPU 命令を実行しようとした時に CPU に例外を生成してもらって FPU のコンテキストを切り替えする方法で、 FPU を使わないスレッドに切り替えた場合は例外が発生しないので何もしくて済みます。
具体的には CR0 の TS フラグを 1 にセットすると 例外 07 (#NM) が発生するので、例外ハンドラで TS フラグをリセットした後 FPU のレジスタ退避・復帰を行います。
x86 のタスク機能を使って切り替えた場合は TS フラグが自動で 1 になりますが、 x64 ではこの方法が使えないのでコンテキストスイッチの時に手動で TS フラグをセットする処理を入れます。

SSE に対応する場合は初期化時に CR4 の OSFXSR ビットをセットし、レジスタの退避・復帰には FXSAVE 命令や FXRSTOR 命令を使います。
ちなみにこのフラグの名前 SSE と関連がさっぱり不明ですが、実は SSE 対応 CPU は SSE モードの有効無効を切り替えるというわけではなく、 SSE は常に有効であるが OS が FXSave/FXRstor 命令で責任を持ってコンテキストスイッチの処理をするから拡張レジスタにアクセスできることを CPU に通知するフラグとなっています。

0.3 までは何も考えず SSE が使えていましたが、これは x64 が ABI レベルで SSE の存在を保証しているため UEFI が自動で OSFXSR フラグをセットしていたからで、それでコンテキストスイッチで問題が発生しなかったのはメインスレッド以外 SSE を使用していなかったためです。

とつぜんの #UD

SSE のコンテキストスイッチに対応したので SSE を利用するスレッドを並べてデモを作っていたところ、謎のタイミングで #UD 例外が・・・

例外の発生するアドレスを調べてみると longjmp 関数から戻ってきたアドレスに longjmp 関数自体に noreturn 属性がついているために ud2 命令でトラップがかけられていた例外でした。
そもそも longjmp 関数は jmpbuf の中身を展開してジャンプしてるのでこの関数が戻ることは決してないはずですが・・・

調べた結果、コンテキストスイッチで longjmp 関数が呼び出されてる間に setjmp 関数が重複して呼び出されて jmpbuf が破壊されてるようでした。割り込みが怪しかったので CLI 命令で割り込み禁止してもかわらず。

試行錯誤の結果、

ということで、コンテキストスイッチそのものを排他制御して同時にコンテキストスイッチが発生しないようにすることでこのバグは収拾し、無事に複数のスレッドで動作するようになりました。
排他制御大事

f:id:neriring16:20181029000053p:plain

この対応で数日かかったので何をしようとしていたのか忘れてしまいました。