myos では Null Pointer Exception は発生しません。
理由は2つあって、言語に Rust を採用しているというのと、 Null Pointer Exception が発生するようにページングを設定していないからです。
事件編
Rust には Null Pointer Exception によく似た別のエラーがあります。
それは、 Option<T>
の None
に対して unwrap()
することで発生する panic です。
Rust では unwrap()
はお行儀の悪い方法なので基本的に他の手段を検討するべきです。
しかし、言語仕様上変数や戻り値を Option
にしないといけないことがしばしばあり、本来 None
が返ってくることはないのでハンドリングが面倒で unwrap()
を使ってしまいます。*1
そして、想定外の現象が起きた時に、本来起こり得ないはずの panic が発生します。
最近それが発生しました。 WebAssembly で特定の条件を満たすと current_thread の current_personality を取得する処理で発生します。
WebAssembly のシステムコール呼び出しをするとき、システムコール関数はランタイムのインスタンスを持っていないので current_thread の current_personality から取得する必要があります。
システムコールを呼び出すのは WebAssembly の内部だけなので current_personality (Option<Box<dyn Personality>>
) が設定されているはずです。
しかし、現実には稀に None
を返して unwrap()
に失敗しました。
解決編
さて、 current_thread を知っているのは誰でしょうか?もちろんスケジューラです。
ただし、ちょっと注意が必要です。
myos のスケジューラーは SMP に対応していて、それぞれのコアが別々のスレッドを実行しています。 つまり、コアごとに実行中のスレッドを管理する必要があります。
現在実行中のスレッドを調べるには、現在実行中の CPU コアの ID を取得する必要があります。 そして、コア ID からコア個別のデータを探して現在実行中のスレッドを特定する必要があります。
以前の myos ではこの処理で割り込みを禁止していませんでした。 それによってどんなことが起きるでしょうか?
現在実行中の CPU コアを特定してコア個別のデータから現在実行中のスレッドを調べる一連の処理の途中でたまたま割り込みが発生した場合、コンテキストスイッチが発生してスレッドキューに戻されることがあります。 そしてスレッドキューに実行待ちのスレッドが多かったりたまたま別のコアがコンテキストスイッチをした場合、最初に実行していたコアとは別のコアでスレッドが復帰する可能性があります。
これらの条件が重なった時、現在実行中のコアとは別のコアのデータを読み出し、現在実行中のスレッドを誤判別するという現象が発生します。
色々な偶然が重ならないと発生しないので確率は低いですが、割り込みやコンテキストスイッチは一秒間に何回も実行しているのでいつでも発生する可能性があります。
今までこのバグが発覚しなかったのは、そもそも現在実行中のスレッドを取得する処理がそこまで頻繁に実行されなかったためです。 WebAssembly から頻繁に API 呼び出しをして画面を書き換えるテストアプリの実行中にやっと発見されました。
教訓
そもそも現在実行中のスレッドを取得するだけの処理で割り込み禁止したり実行中のコアを調べるのは少々複雑すぎな気がしませんか?
毎回割り込み禁止にするのは良くないので、スタックポインタから逆算できたり通常変更しないレジスタから取得できるようにした方がいい気もします。
*1:似たような事例として Result の unwrap もあります