趣味はOS自作、自動車業界でエンジニアをやっているだいみょーじん氏

だいみょーじん氏(以下、だいみょーじん):では、「QEMUのバグを見つけてパッチを送った話」をお話しします。今回の発表は、まず自己紹介をしてバグ発見の経緯をお話しして、その後にバグに関する考察と原因調査、そして修正パッチ、まとめという流れで発表をしていきます。

まずは自己紹介です。だいみょーじんと申します。自動車業界でエンジニアをやっていて、趣味はOS自作です。こんな感じのOSを作っています。最近のUEFI(Unified Extensible Firmware Interface)ではなく、古き良きレガシーBIOS(Basic Input Output System)で動くOSになっております。

自作OSに浮動小数点演算装置を用いた計算アプリを実装した時に逆ハイゼンバグに遭遇

今回のバグ発見の経緯です。自作OSにx87FPU、浮動小数点演算装置を用いた計算アプリを実装しました。(スライドを示して)こんな感じで簡単な四則演算や三角関数が一応計算できています。

ただ、ある状況で変なバグが起きたんですね。図のように、QEMUの上で自作OSが動いていて、OSがFPU(Floating Point Unit)、浮動小数点演算装置に向けてなにか演算命令を送って、FPUは演算結果を返して、OSがそれを出力をします。これは正しい値なんですね。ただ、GDBからQEMU上で動いているOSにアタッチしてFPUの演算結果をこのGDBから表示させるとなんか値がおかしい。

OSの出力は正しいのに、デバッガでFPUの中身を確かめるとおかしな値になっているという逆ハイゼンバグに遭遇しました。

(スライドを示して)これが実際のバグなんですが、ここらへんでfninit命令でFPUを初期化して、このあとFPUのスタックにいろいろな浮動小数の値をプッシュしています。GDBのinfo floatコマンドでFPUの中身を表示できるんですが、例えばこれはFPUにプッシュした値としてe+2165とか、とてつもない値が入っています。

あともう1つ、一番下にControl Wordというのがあります。ここも本来であれば0x037fになるはずなんですが、これもまったく違う値になっている。よく見ると変なところに怪しげな037fがある。これをよく覚えておいてください。こんな感じのバグに遭遇しました。

このバグに関する考察です。OS自体は正しい値を出せているので、OSやFPU自体は間違っていないと思うんですよ。となると、(スライドの)赤い線上のどこかで値がおかしくなっていると思うんですよね。このバグをどうやって調査しようかなと思ったんですが、とりあえずもう1つGDBを起動して、ここらへんで値がどう処理されているのかを追ってみようと思いました。

main関数から一歩ずつ潜って原因を調査

では調べていきましょう。「デバッグするぞー!」ということで、GDBのソースなんか読んだことはなかったのですが、気合いでmain関数から一歩ずつ潜っていくと、いろいろと見えてきました。

(スライドを示して)このstart_event_loopという関数の中に無限ループがあって、さらにその中にgdb_do_one_eventという関数があって、どうやらここでGDBに打ち込まれたコマンドを1つずつ処理しているっぽいです。

さらに潜っていくと、GDBのinfo floatコマンドでFPUの各レジスタの値を表示する関数を見つけました。この関数の中で出力される各FPUレジスタの値の出所をさらに辿っていくと、レジスタの溜まり場みたいなものがあったんですね。(スライドを示して)例えばこれは0202とあって、EFLAGSレジスタっぽいものとか、7c00のこれはEIPレジスタっぽいです。

ここの0000が、どうやらControl Wordとして出力されているようなんですが、ここにその本来のControl Wordの値である037f。怪しげな037fがここにあります。

さらにレジスタの溜まり場の出所を辿ると、どうやらgパケットなるものでQEMUからGDBに各レジスタの値が送られていることがわかりました。これがそのgパケットの中身です。文字列で送られているのですが、ちゃんとEIPもいるし、EFLAGSっぽいのもいるし、ここの0000がFPUのControl Wordとして表示されているんです。

だけどエンディアンでちょっと逆になっていますが、相変わらずこの怪しげな037f、本来のControl Wordの値っぽいものがあります。

さらに調査を進めると、QEMUはgパケットのデータ構造を定義しているXMLファイルをGDBに送っていて、GDBはそれに基づきgパケットから各レジスタの値を抽出しているということがわかりました。そのXMLをよく読んでみると、EFERレジスタの大きさに関して矛盾した記述がありました。(スライドの)上の赤いほうは、EFERレジスタの値のサイズが8byteと言っていて、下の緑のほうはEFERレジスタのbitsizeが32bit、つまり4byteであると言っています。

QEMUとGDBの間でEFERレジスタの大きさの合意が取れていないのでは?

ここで1つの仮説が出てきます。QEMUとGDBの間で、EFERレジスタの大きさの合意が取れていないと仮定すると、辻褄が合います。(スライドを示して)この図ではgパケットがあり、上側はQEMUがどのようにgパケットを構築しているのかを表しています。それに対して下側は、GDBがgパケットを受け取って、それをどのような構造として解釈しているのかを表しています。

途中まで両者は一致しているんですが、EFERの大きさの合意が取れていないので、これ以降の全部のレジスタが4byteずれてしまっている。そのため、FPUのCTRL Wordレジスタに、ちゃんと037fと入れたのにGDBではそれがR7の一部分だと解釈されてしまって、それがその怪しげな037fになったんじゃないかと考えました。

スタック内の要素の指定方法が混同したことにより新たなバグが発生

仮説が立ったところで、EFERのレジスタは4byteであると修正して、もう一度実行してみると、こうなりました。先ほどと同じようにFPUをfninitで初期化して、先ほどと同じ数値をFPUスタックにプッシュします。info floatコマンドでFPUの中身を表示してみると、Control Wordはきちんと037fになりました。スタックの中にもまともな値が入っています。

ということでちゃんと直ったので、パッチを送って無事にマスターブランチにマージされました。これで「めでたしめでたし」といきたいところなんですが、本当に直ったのでしょうか? もう一度確認してみましょう。

まず、Control Wordはきちんと037fになっています。スタックの中にもまともな値が入っています。ですが、実は位置がずれています。FPUスタックは上から下に向けて伸びていく。要するにプッシュされた順番どおりに出力されるはずなので、実際には(スライドを示して)こういう順番にならないといけません。

これに関しては、バグの調査でソースコードを読んでいた段階で「ここおかしくないか?」と薄々気づいてはいました。これは何が起きているかというと、スタック内の要素の指定方法は2つあって、絶対位置指定のR0~R7というものと、スタックのトップからの相対位置指定であるST0~ST7というものがあります。

つまりこの相対指定は、プッシュしたりポップしたりするたびに、全体が上に行ったり下に行ったりする。現在のスタックのトップは、この矢印で示されたR1なので、そのRxやSTxがこう対応しているのですが、このRxやSTxがごちゃごちゃにされてずれているというのが起きています。

調べたところ、これもやはりQEMUとGDBの間のgパケットの解釈違いでした。Rxでgパケットに乗せて送信しているのに対して、受信したgパケットは相対位置指定で入っているものだとGDBが解釈してしまったために、そのずれが起きてしまっていました。

gパケットの仕様的には、どうやら相対位置指定で送るのが正しいということだったので、QEMUで適切な変換を施すことで、正しい順序でFPUの各要素を表示することができるようになりました。このパッチはまだマージされていませんが、そのうちマージされると思います。

バグり散らかしている部分をこれからも直していきたい

最後にまとめです。OSを自作していたらGDBでQEMUをデバッグした時のみ値がバグるという逆ハイゼンバグに遭遇しました。調査したところ、x86アーキテクチャのFPUまわりのQEMUとGDBの連携が壊滅的にバグっていたので直しました。だいぶマシにはなりましたが、まだまだ細かい部分でバグり散らかしているので少しずつ直していこうと考えています。ご清聴ありがとうございました。

他にバグり散らかしているところは?

司会者:ありがとうございました。時間ピッタリでありがとうございます。「バグり散らかしている」(笑)。

(一同笑)

司会者:でも、defineがずれているのは「そっか」と思いましたけど、扱いが違うと気づくのがすごいなと思いました。きちんと把握していないと「わからない」で、直ったら「直った!」で終わっちゃいそうですね。

(一同笑)

司会者:そう思っていたら、最後のオチが「バグり散らかしている」で、ちょっとおもしろかったですね。(コメントを見て)「そうそう、QEMU意外とバグってるんだよ。A64FX(富岳CPU)のエミュレーションをさせていると、コーナーケースでいろいろと計算を間違うので困った。SVE512とかは世界に1つしかないから仕方ないといえばそれはそう」。「アーキテクチャ固有の話は、ユーザーが少ない、さらにデバッガを使っている人が少ない、誰も気づいている人いない」みたいな(笑)。

「ユーザーが少ないとバグりがち」。ちなみにこのバグり散らかしているのは、リストにしていたりしませんか?

だいみょーじん:リストは特に作っていませんが、(スライドを示して)例えばここは全部Validになっているとか。

司会者:確かに(笑)。

だいみょーじん:他にもいろいろありますね。

司会者:お気づきでしょうか?ここがすべてValidになっている……確かに(笑)。

(一同笑)

司会者:ありがとうございました。