OpenGLでウィンドウのクローズにちょっとつまづいた話
自作エミュレータで自作OSを動かしてみようとした話 - sksat’s diary で書いたように、OpenGLを使ってQEMUやBochsのようなx86エミュレータっぽいものを作っているのですが、ウィンドウのクローズに関してちょっとつまづいたのでメモしておきます。
何が起こったのか
今僕が作っているエミュレータでは、画面表示のエミュレーションにOpenGL(freeglut)を使っているのですが、機械語の実行が遅れると困るので、OpenGLの初期設定やメッセージループは別のスレッドで実行しています。ちょっとGUIスレッドの部分のソースコードを一部抜粋すると、
void GUI::ThreadProc(){ int argc = 1; char *argv = new char[1]; glutInit(&argc, &argv); // fake command-line args glutInitDisplayMode(GLUT_RGBA); glutInitWindowSize(scrnx, scrny); hMainWin = glutCreateWindow("display"); while(msgflg){ //message loop glutMainLoopEvent(); this->display(); } glutDestroyWindow(hMainWin); } GUI::~GUI(){ msgflg = false; hThread->detach(); delete hThread; }
こんなかんじになります。GUIというクラスにGUIの制御を任せていて、GUIクラスのデストラクタが呼ばれるとフラグがfalseになってメッセージループが停止し、glutDestroyWindow()
が呼ばれてウィンドウが閉じるようになっています。
そして、このGUIクラスを制御しているmain.cppは、
Emulator *emu; GUI *gui; Display *disp; int main(int argc, char **argv){ emu = new Emulator(); disp = new Display(emu->memory + VRAM_ADDR); gui = new GUI(disp); /* 中略 */ gui->OpenWindow(); //これがスレッドを起動させる while(true){ /* 中略(機械語の実行) */ } delete emu; delete gui; delete disp; }
というようになっています。つまり、エミュレータが途中で停止すれば自動的にウィンドウが閉じられるというわけです。
では、EAXに0xffを代入するだけの簡単なプログラム(MOV EAX,0xff
)をこのエミュレータで実行してみましょう。
一瞬で終了しました。それもそのはず、メインスレッドで、MOV EAX,0xff
を実行したすぐあと、EIPの指すメモリに実装されていない命令がある(正確には、なにも命令を配置していないので0x00があり、また0x00の機械語を実装していない)ので、エラーを出してメインスレッドの機械語実行ループが終了、delete gui;
によりGUIクラスのデストラクタ内で、glutDestroyWindow()
が呼ばれ、ウィンドウが目に見える間もなく閉じられてしまっているからです。
これだとせっかく実装したウィンドウが見えないので、ウィンドウが見えるように、実行させる機械語にJMP 0x7c00
を追加してループさせてみましょう(一応x86エミュレータなので、プログラムは0x7c00番地にロードする仕様になっている)。
うん、ちゃんと画面描画できてますね〜、って、ん???
あれ、これ、おかしくないですか?ウィンドウ閉じたら機械語の実行も止まるなんてコード、僕書いた覚えありませんよ?
まあ、実際のところ☓ボタンを押したらエミュレータには終了してもらいたいですが、それでも、意図しない動作というのは嫌なものです。
というか、最後にレジスタの値を表示するのができていないので、これはかなり重大なバグと言っていいでしょう。メモリもちゃんと解放できてるのか心配です。
うーん、何がダメなんでしょうか。 ウィンドウの細かいことは全部OpenGlに丸投げしてしまっていますが、☓ボタンが押された時にどのような処理がされているのかちょっと追ってみましょう。
while(msgflg){ //message loop glutMainLoopEvent(); this->display(); } glutDestroyWindow(hMainWin);
メッセージループに入っている間はウィンドウの処理は全部glutMainLoopEvent()
でした...
つまり、glutMainLoopEvent()
かその中で呼ばれている関数が☓ボタンの処理を行っているはずです。
しかし、考えてみると、☓ボタンが押されたらメッセージループは終了するべきですが、glutMainLoopEvent()``内からメッセージループを終了させることができないようになっています。
もしかしたら、
glutMainLoop()では
glutMainLoopEvent()の返り値を見てメッセージループを終了させているのかもしれません。
ということで、
glutMainLoopEvent()```の返り値を見てみます。
とりあえずintでいいかなと思ったんですが、voidだったんですね...コンパイラに怒られてしまいました。
glutMainLoopEvent()
の返り値が無いとすると、次に考えられるのは、☓ボタンが押されるというイベントを処理するコールバック関数を登録する関数があるのではないか、ということです。
ではどんな関数があるのか、たまたますぐ近くにlibfreeglut.a
が転がっていたので(Windows用な気もするけれど)、objdumpで調べてみます。
objdump -D libfreeglut.a
「お〜、こんな関数あったんだ〜」と思いながらスクロールしていくと、おや? なんかそれっぽい関数名が出てきました。コールバック関数は「glut***Func」という名前なので、これは怪しいです。
ということで、さっそく関数を登録してみましょう。
void close(){ cout<<"close"<<endl; } void GUI::ThreadProc(){ /* 中略 */ glutCloseFunc(close); while(msgflg){ glutMainLoopEvent(); } glutDestroyWindow(hMainWin); }
...どこにいるんだよclose...
どこかにいるかもしれませんので見てみます。./vm test02.bin | grep close
うん、ちゃんといました。ウィンドウが閉じるときにちゃんと呼ばれているようです。しかし、結局はプロセスが終了しています。closeイベントが発生してglutCloseFuncで登録した関数が呼ばれた後にexit()
かなにかを呼んでいるのでしょうか。
exit()
が呼ばれているかどうかが気になったので、ちょっと`ltrace ./vm test02.bin
とかやってみました。
わからん...ostreamとか書いてあるのは標準出力関係か...マルチスレッドだしな...(しかしシングルスレッドで試してみてもよく分からなかった..力不足...)
よく分からなかったのですが、せっかくウィンドウのクローズ時にcloseが呼ばれるようになったので、close内でglutDestroyWindow()
してみます。
int g_msgflg, g_hMainWin; void close(){ cout<<"close"<<endl; glutDestroyWindow(g_hMainWin); g_msgflg = false; } //ThreadProcの一部 void GUI::ThreadProc(){ g_msgflg = true; while(g_msgflg){ glutMainLoopEvent(); this->display(); } //glutDestroyWindw(hMainWin); }
ハードコーディングは気にしないでください...
では、実行してみます。
正常終了どころかコアダンプ...むむう...
後日談、というか、今回のオチ
まだ調べ途中で結局できていないのですが、ここまで読んでくれた人ならお分かりでしょう。
「公式ドキュメント見ろよ!!! というか、関数名調べるだけならヘッダ見ればいいでしょ!!!」
はい、全くもってその通りです。CTFの本読んだからって調子に乗りました。