KazuminEngine

プログラマーの日記

エミュレーターの作り方(はりぼてOSが動く)

※この記事は、どんどん書き換えていきます。つまり、まだ途中です。

私について

私は、@warugaki_k_kと申します。

はじめに

この記事は、はりぼてOSが動くi386 PCエミュレータの作り方の解説記事です。ただし、「OtakuAssembly vol.1@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)や「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!内田公太(著) 上川大介(著)に書いていることは重複して書かないつもりです。なので、この記事を読む前に上記の本で基本的なエミュレーターの作り方を学んでから、本記事を読むのが筆者のおすすめです。従って、この記事は、上記の本には書かれていない、はりぼてOSを動かすための技術やtipsを説明します。読んでいて、分からないなと思うところがあると思うのですが、その分からないまま先を読めば、疑問の答えが書いてあることが多数あります。分からないまま読んでいきましょう。いずれ、答えば出てくるので。

ただし、パソコンの挙動を完全理解しているわけではないので、筆者のエミュレーターは、間違えているかもしれません。もし、間違いや、こうしたほうがいいよ、などはコメント欄に書いてもらえると嬉しいです。

エミュレータを作る上でPCの動作の仕組みをよく理解していなければいけません。 本記事の目的ですが、筆者は、はりぼてOSが動くエミュレータの仕組みや技術を知ったり実際に作ったりすることにより、PCの仕組みを理解する助けになればいいと思ってます。また、他のエミュレーターを作る際の参考にしてもらえば幸いです。実際に、筆者は、低レイヤーに興味があり、実際に作ることによりPCの仕組みを理解したいのでエミュレーターを作り始めました。

(本記事は、SecHack365 3期 学習駆動の成果物になります。SecHack365については、こちらをご覧ください。)

筆者の開発したエミュレーターについて

筆者の開発しているエミュレーターのURLはこちらです。 

GitHub - kazuminn/EEMU

2020年2月9日時点で、1500行程です。

筆者はC++で実装しました。ここに載せるコードもC++です。他の言語でエミュレーターを書きたい人は、多言語に翻訳しよう。

この記事に掲載されているコードは全て、EEMUのものです。提示されている以外のコードを見たければ、githubの方を見ましょう。

開発のススメ

はりぼてOSが動くPCエミュレーターを作るのであれば、以下の資料に目を通せば、できるかな?(載せないけど、以下の他にweb上の記事を私は読んだ。)RISC-Vとかになるとわからないので、それを実装したければ、詳しい人に聞いてください。

オペコードの実装は、以下のSDMや、先人たちの(私を含む)コードを読んでいくと良いでしょう。少なくとも私は、そうしました。

・このブログ。

OtakuAssembly vol.1@OtakuAssembly(著)(自作エミュレーターでOSを動かす章)

・「自作エミュレータで学ぶx86アーキテクチャ-コンピュータが動く仕組みを徹底理解!内田公太(著) 上川大介(著)

32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)蒲地輝尚 (著) こちらは、kindleで買えば良さそう。

intel SDM

はりぼてOSとは?

本題の前に「はりぼてOS」が何かについて説明します。本「30日でできる! OS自作入門川合秀実(著)で作っていくOSです。OSにしては非常に小さいコード量のOSです。なぜ、はりぼてOSをエミュレータで動かそうと思ったのかは、本を読めば、実装がすべてわかるからです。流石に、Linux系OSを動かそうと思えば、技術も複雑だし、全体を把握できません。

エミュレーター作りにおいて、その上で、動かすOSの挙動を把握することは、良い開発をすることに繋がります。例えば、何か命令にバグがあり、デバックするときにOS側の値が正しい値になっているかどうか確認しながら開発できると、よりスムーズな開発に繋がります。なので、はりぼて本はよく読んでいたほうがいいです。私は、あまり読みませんでしたが。

プロテクトモード突入するための、16bitモード実装

その前に

さて、エミュレーター作りの説明に入っていきます。 まず、エミュレーターにただ単に命令をたくさん実装すれば、エミュレーターが完成するわけではありません。 なぜなら、はりぼてOSは、Linuxネイティブなどで動くOSではなく、ベアメタルな環境でも動くOSだからです。はりぼてOSは、起動直後は16bitモードで動きます。なので、普通は、エミュレーターに16bit命令とBIOSを作ってあげなければ、いけません。

しかし、オススメの方法あります。それは、16bitモードで動くのを全部無視することです。(16bitモードでの作業をc言語で実装することです。)それなら、いきなり32bitで始めることができます。つまりプロテクトモードから始めることができます。ここで言うオススメの意味は、 「もし16bitやBIOSを実装することになれば、エミュレータとして完成度は上がりそうだけど、作業量はさらに増えて、期間中に終わらなくなるから。もしくは、モチベーションが無くなり、完成されることなく終わるのを無くすため。」です。筆者の環境ではSecHack365と言う限られた約1年の期間だったので、オススメの方法をとりました。読者様がもし、エミュレータを作ることになれば、モチベーションと相談しながら、オススメの方法を選ぶのもありですね。エミュレーターは結構時間がかかると思います。 ただ、筆者が実装してないので、16bitモードやBIOSの実装がわかりません。したがって、この記事には16bitモードやBIOSについては、記述しません。

少し具体的な話

少し具体的には、ipl.binだけではなく、asmhead.binも16ビットのアセンブラなので、これを全部スキップしたいのです。 だからiplとasmheadが何をやっているのかを理解して、これと同等の処理をエミュレータ側でやってやる必要があります。

実装についての話

より具体的な実装の話をします。ちょっと難しいので、頑張ってついて来てね。 以下のことを実装していくと、この章でやりたいことができます。 bootpackっていうラベルがわかる必要があります。それは、0xC390です。 エミュレータは、はりぼてOSモードフラグを付けて起動した場合は、まずディスクイメージを 0x00100000 へ読み込んでやります。 その次に、その 0x00104390 からの512KBを 0x00280000 へコピーします。 なんで 0x00104390 なのかというと、 c390 から 8000 を引いて、 100000を足したから。 ディスクイメージ内で、bootpackがどこにあるかというと、先頭から0x4390バイトのところから。 だからそれに100000を足せば、メモリ上のでのbootpackの場所がわかる。

ESPの値を0x0028000cから読み込む。 なぜ、0x00280000なのかを簡単に言うと、asmhead.nasに、 "BOTPAK EQU 0x00280000 ; bootpackのロード先" って書いてあって、はりぼてOSはそういう想定でリンクしてあるから。

0x00280010 (=BOTPAK+16)にはバイト数が4バイトで書いてあるらしいのでそれを読む。 0x00280014 (=BOTPAK+20)には転送元の場所が4バイトで書いてあるらしいのでそれを読む。読んだらそれに0x00280000を足す。 0x0028000c (=BOTPAK+12)には転送先の場所が4バイトで書いてあるらしいのでそれを読む。 これでmemcpyをする。

これができたら、CSとDS/ES/SSのベースアドレスをエミュレータ側で設定して、 (ちなみにDS/ES/SSのベースは0x00000000。CSは0x00280000。) EIP=0x1bにすれば、ついに32bitモードでの実行が始まるのだだだだだだだだーーーー!

なぜ、 0x8000なのかですが、8000は16ビットモードの時に、ディスクイメージを読み込むの時の先頭のアドレス。 だから16ビットモードのプログラム(iplとasmhead)では、ディスクイメージが8000から始まる想定でいろいろ計算されているからです。0xc390も、それが前提での値です。

なぜ、ディスクイメージを16ビットモードの時は8000にして、32ビットモードの時は0x00100000にしたのかですが、16ビットモードの時は、100000へ読み込ませることができないから、しょうがないので8000へ読み込んでいた。それで32ビットモードになった瞬間に、100000へ再転送している。

memcpyしているけど、それは何をしているのかと言うと。 C言語では、文字列とかの定数を所定のアドレスに転送しないと満足に実行できない仕組みで、普通それはOSがアプリ起動時に自動でやることになっているんだ。 でも今の段階ではOSなんていないから、asmheadがbootpack.cのやつをやってあげていたんだけど、それを代行しているわけ。

このmemcpyする値は、決め打ちで決めていいかというとそうではない。 なぜなら将来はりぼてOSを自由に改造していくとこの値は変わってしまうから。 「どこに書いてあるか」は変わらないのだけど、「その値はいくつか」は変わるので、エミュレータも毎回読み取ってそれに基づいてコピーするのがいい。

EIP = 0x1bはbootpack.lstの何行目を示すんでしょうか?といった疑問がありますね。 答えですが、bootpack.sysはbootpack.hrbの前にasmhead.binをただくっつけたものですが、 そのbootpack.hrbの先頭にはシグネチャやデータ転送用の情報が書いてあって、0x1bもその一部です。 0x1bには0xe9(だったかな)が書いてあって、これはJMP命令の機械語になっています。 どこへ飛ぶのかというとHariStartup(だったかな)へ飛びます。これはライブラリに含まれています。 そのHariStartupは、現状ではHariMainを呼ぶだけの関数になっていて、だからすぐにHariMainに行くわけです。 つまり0x1bから実行を開始すればHariMainへ行けるようになっているのです。

コード

これは、実際に私のエミュレータにある該当コードです。上記の説明がわかれば、読めると思います。頑張って読んでみてください。

emu->LoadBinary(argv[1], 0x100000, 1440 * 1024);
std::memcpy(&emu->memory[0x280000], &emu->memory[0x104390], 512 * 1024);

uint32_t source;
uint32_t dest;
uint32_t interval;
std::memcpy(&source, &emu->memory[0x280014], 4);
std::memcpy(&dest, &emu->memory[0x28000c], 4 ); source += 0x280000;
std::memcpy(&interval, &emu->memory[0x280010], 4);

std::memcpy(&emu->memory[dest], &emu->memory[source], interval);

emu->EIP = 0x1b;
emu->ESP = dest;

vram

「はりぼてOS」は、本来はIPLに書かれている16bitモード用のプログラムから起動が始まります。 その16bitプログラムが、ビデオBIOSを呼び出して、VRAMアドレスやscrnxやscrnxやビデオモードを教えてもらっています。

しかし、

今回は、IPLな16bit処理を飛ばしているので、エミュレーター側から、OSに教えてあげる必要があります。これは、非常に簡単です。 以下のようにはりぼてOSは書かれているのですが、このc言語が読める人はすぐにわかったでしょう。BOOTINFOが0x0ff0に確保されているので、0xffxに何か書き込めば、それがOS側の各々の変数に書き込まれます。 これが番地の対応です。charは1バイトだし、charが二つ確保された後にあるので、0xff2になってます。ここは、できれば、計算して見ましょう。

vram -> 0xff8

vmode -> 0xff2

scrnx -> 0xff4

scrny -> 0xff6

ですね。

struct BOOTINFO { /* 0x0ff0-0x0fff */
    char cyls; /* ブートセクタはどこまでディスクを読んだのか */
    char leds; /* ブート時のキーボードのLEDの状態 */
    char vmode; /* ビデオモード  何ビットカラーか */
    char reserve;
    short scrnx, scrny; /* 画面解像度 */
    char *vram;
};
#define ADR_BOOTINFO    0x00000ff0
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;

コード

筆者のコードでは以下のようになっています。

emu->memory[0xff2] = 8;
emu->SetMemory16(0xff4, 320);
emu->SetMemory16(0xff6, 200);
emu->SetMemory32(0xff8, 0xa0000);

セグメンテーション

上記の説明にCSが出て来ましたが、これがセグメンテーションの要です。 セグメンテーションについての説明ですが、割愛させていただきます。30日OS本やネット記事を読んでください。 エミュレータでは以下のように実装されています。

sgregs[1].base = 0x280000; //sgregs[1] はCS

far-jump命令

[0] まあ動作的には、指定されたメモリ(もしくはレジスタから)16bitを読み込んで、TRというCPUの内部レジスタに記憶しておくだけでいいのだけどね。あと、baseとlimitも設定しておきましょう。こんな風に、 base = (tssdesc.base_h << 24) + (tssdesc.base_m << 16) + tssdesc.base_l; limit = (tssdesc.limit_h << 16) + tssdesc.limit_l;

[1]次は、本題の FF /5 の実装に入ります。 FF /5 命令はメモリオペランドになっているはずなので、そこから6バイトを読み取って、EIP部とCS部に分けます。

[2]そしてCS部はセグメントセレクタなので、まず下位2ビットを無視するために、0xfffcでANDします。 もしここで結果が0になってしまったら、それはヌルセレクタなので、本物のCPUでは一般保護例外(INT 0xd)になるのですが、まあエミュレータはそこまではチェックしなくていいでしょう。

[3]次にbit2が0か1かを判定します。 もし1だったら、LDTを参照しなければいけないのですが、今はLDTの知識がないので、エラー終了させておいてください。

[4]0だったらGDTなので、GDTのリミットとセグメントセレクタ+7を比較して、リミットのほうが大きいかもしくは等しいことを確認し、 もしそうでなかったら、本物のCPUでは一般保護例外(INT 0xd)なんですが、まあめんどくさいからやらなくていいかな、これも。

[5]次にGDTのベースとセグメントセレクタを加算します。このアドレスからの8バイトにセグメントの情報が書いてあるのです。 そしてセグメントの属性は、+5のところにあるので、つまり、[GDTのベース+セグメントセレクタの下位2ビットを0でつぶしたもの+5]の1バイトを読み取ってください。

[6]その1バイトを0x9fでANDして、この値でセグメンテーションのタイプを判定します。

0x80 : 無効 → これだったら本物のCPUは一般保護例外
0x81 : 286用(16ビット)のTSS ・・・ 「はりぼてOS」では使わない
0x82 : LDT
0x83 : 286用(16ビット)のTSSでビジー状態
0x84 : 286用コールゲート
0x85 : タスクゲート
0x86 : 286用割り込みゲート
0x87 : 286用トラップゲート
0x88 : 未定義
0x89 : 386用(32ビット)のTSS
0x8a : 未定義
0x8b : 386用(32ビット)のTSSでビジー状態
0x8c : 386用コールゲート
0x8d : 未定義
0x8e : 386用割り込みゲート
0x8f : 386用トラップゲート
0x90 : リードオンリー・データセグメント(A=0)
0x91 : リードオンリー・データセグメント(A=1)
0x92 : リードライト・データセグメント(A=0)
0x93 : リードライト・データセグメント(A=1)
0x94 : 未定義
0x95 : 未定義
0x96 : リードライト・下方伸長型データセグメント(A=0)
0x97 : リードライト・下方伸長型データセグメント(A=1)
0x98 : 実行オンリー・コードセグメント(A=0)
0x99 : 実行オンリー・コードセグメント(A=1)
0x9a : 実行読み出し許可・コードセグメント(A=0)
0x9b : 実行読み出し許可・コードセグメント(A=1)
0x9c : 特権例外型・実行オンリー・コードセグメント(A=0)
0x9d : 特権例外型・実行オンリー・コードセグメント(A=1)
0x9e : 特権例外型・実行読み出し許可・コードセグメント(A=0)
0x9f : 特権例外型・実行読み出し許可・コードセグメント(A=1)

ということで、タイプが0x98~0x9fなら、とび先は純粋なコードセグメントなので、CSを更新して、EIPも更新して、これでfarjmpは終了です。 タイプが0x89か0x8bなら、タスクスイッチすることになります。この場合、この時点でCSやEIPを更新してはいけません。

[7] ということで、以下はタイプが0x89か0x8bの場合で、タスクスイッチするケースです。 タスクスイッチに先立って、今のレジスタ値を今のTSSに保存します。 なお、この時点でeipはfar-jmp命令の次の命令を指しています。 今のTSSがどこにあるかは、TRを見ればわかります。

struct TSS32 {
    int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    int es, cs, ss, ds, fs, gs;
    int ldtr, iomap;
};

保存対象となるのは、この中の、eip, eflags, eax~edi, es~gsです。 保存が終わったら、TRを更新し、移行先のタスクのTSSから、cr3, eip, eflags, eax~edi, es~gs, ldtrを読み込みます。 あとは、新しいCS:EIPより実行を始めればよいのです。

※[3]はCS部のbit2ですか。また、この話に出てくる、セグメントレジスタは全部CS部のことです。 セグメントレジスタは、調べましょう。もしかすると、筆者が後で書くかもしれません。このままだったら、書いていません。

実装を詳しく見たい人は、githubのEEMUのinstruction32.cppファイルのfarmup()を見てください。

メモリチェックルーチン

はりぼてOSは、PC(エミュレーター)のメモリ量をチェックしに行くのですが、これに失敗してしまうため、決め打ちでメモリ量をOSに教えておきます。

つまり、実装では、 はりぼてOSのbootpack.cの67行目のmemtest()は呼び出さなくて、memtotal = 32 * 1024 * 1024; のように決め打ちにしておきます。 (私の環境では、memtest()に失敗するので、1が帰ってきていました。)

このURL先のようにしておきます。 https://github.com/kazuminn/haribote/blob/master/30_day/harib27f/haribote/bootpack.c#L67

LGDT、GDTR

こちらはセグメンテーションの為にも必要です。

LGDT命令は、指定されたメモリアドレスから、6バイトを読み込んで、GDTRという48bitのレジスタにしまっておく命令です。 この48bitのうち、最初の2バイトはGDTのリミットで、後ろの4バイトがGDTのベースになります。

こちらが実装です。

void lgdt_m32(Emulator *emu, ModRM *modrm) {
    uint32_t m48 = modrm->get_m();
    uint16_t limit = emu->GetMemory16(m48);
    uint32_t base = emu->GetMemory32(m48 + 2);

    emu->set_gdtr(base, limit);
}

IN命令

IN命令が何をしているかと言うと、ポートからの入力を受け付けます。 そんなこと言ってもわからないのと思いますが、直感手に説明すると、エミュレーターが側から、OS側に入力があるときに使われます。

PortIO

PortIOの実装と設計の話ですが、単純にポートのアドレスに対して、指定された処理(関数)を呼ぶ実装にすれば良いのですが、、c++の言語特性上そううまい設計はできません。指定された処理の中にはデバイスごとの関数があり、それはオブジェクトごとにまたがっているので、呼び出すときに工夫が必要です。なので、私の場合はPortIOクラスを作成して、そこにin8関数を作成して、各々のデバイスでin8関数をオーバーライドしてやる感じで、実装しています。そうすると、うまいこと、オブジェクトごとにまたがって呼び出すことができます。

割り込み・PIC

ます、PICの機能はわかりますか?PICの機能:IRQ0~15をシステムに提供する。どの割り込み信号線がどのハードウェアに対応しているかは、たいていは基板上できまる。最近のハードウェアは、その対応をソフトウェアで設定できるけど、「はりぼてOS」はそこまでは対応していないので、タイマはIRQ0とかキーボードはIRQ1とかに固定して考えている。PICがやるのはIRQが来た時に「INT nn」命令を実行するところまでで、ハンドラを呼び出したり登録したりしているのはCPUの機能だよ。 例えばハンドラを設定するのは、IDTの機能だよね。

CPUはCLI/STIで、IRQ割り込みの全部を有効にしたり無効にしたりはできるけれど、PICはIRQの番号ごとに有効無効を設定できるよ。 また割り込みを一度受け付けると、「その割り込み処理を完了しました」ってPICに伝えない限り、次の割り込みはマスクされるよ。 この仕組みがなかったら、CPUが処理しきれない頻度で割り込みが起きた場合に、システムは死ぬしかなくなるよね。

IRQ1の信号が来た。ってことを反応したいのですが、どうすれば良いですか? ->普通は、エミュレータの中に16ビット程度のフラグ(IRQ0~15に対応)を持っておいて、対応するフラグを1にするだけです。 ( これは、0~15のbool配列を作ってやればいいです。これをInterrupt Request Register(IRR)と言います。

割り込み処理の入れ方は、命令実行のループ(=CS:EIPから命令を取ってくる場所)の先頭で、 それらのフラグやPICの設定状態や、eflagsのIFなどを見て、割り込みを発生させるかどうかをチェックすることになります。 実際のCPUもそのタイミングでPICと通信しています。

これをポーリングでチェックするのでしょうか? ->そうです! 本物のCPUも、命令実行のタイミングごとにポーリングしているので(1命令ずつ)、それを真似するのが、もっとも忠実なエミュレーターですよね?! ハードウェアでのポーリングです。 だからソフトウェア的なポーリングとは厳密には違います。 ハードウェアでのポーリングの場合、並行処理ができるので(メインの演算とは別の回路でポーリングしているので)、速度の低下はありません。電力はほんの少し余計に食いますけどね。 

例えば、INT20に対応する割り込みゲートにアクセスをしたいときは、IDTR.base + 20でアクセスすれば良いのでしょうか? →INT 0x20 でどう対応するかを決めるのは、IDTR.baseから始まるゲートディスクリプタを見なければいけないというのはその通りです。 ゲートディスクリプタは、8バイトずつになっているので、それをわかっているのなら、C言語的には IDTR.base + 0x20 と書けます。 しかし単純にアドレスを計算したいのなら(baseが8バイトの構造体へのポインタではなく、単なるintやchar*なら)、

。。。まだ書いている途中。。。

IDTR.base + 0x20*8 になります。

1 input output (IO)

1.1 画面出力

まず、画面描画には、外部のコンピュータグラフィックスライブラリが必要です。筆者は、OpenGLのfreeglutを使っていますが、他のでも大丈夫だと思います。例えば、OpenGLのGLFWなどでも、windowsのものだっていいのです。動かしたい環境に沿ったものを用いてください。

OSがどうやって描画しているのかと簡単に復習すると、PCに付いているVRAMにOSが色番号を書き込むことによって、描画されるのでした。OSは書き込むだけで、描画してくれるのです。このPC(エミュレーター)のVRAMに書き込まれたのを画面に描画するようにエミュレーターを実装してあげなくてはいけません。

と、ここまで書きましたが、画面出力のことは「OtakuAssembly vol.1」に書いてあるので、割愛します。

ちなみに、画面描画のタイミングですが、うまくGUIライブラリと連帯しながら、画面処理とエミュレーターの命令実行処理は並列実行してあげれば問題ありません。

1.3 キーボード

キーボードの実装は結構簡単です。あまり教えることも書くことも、ありません。割り込みなどはすでに実装済みなので、キーボードで実装することはかなり少ないです。

まず、 キーボード(キーボード割り込み)がしたいことを理解してください。それは、OS側が用意しているFIFOバッファに、押したキーのキーコードを入れてあげることです。

なので、そのためのエミュレータの実装についてですが、 エミュレータに対して、キー入力があったら、それをバッファにためておくようにします。 そして命令実行ループの直前で、バッファにデータがたまっていないかどうかをチェックするようにして、 データがあって、かつIF=1で、IRQ-01が許可されれていたら、割り込みを発生させます。

ちなみに、 最終的にFIFOバッファに、押したキーのキーコードを入れるのは、0xECオペコードのIN命令が担います。なので、IN関数を実装する必要があります。OS側にio_in8(IN命令)がアセンブラで実装されていて。そのコードがOS側にキーコードを伝えます。その処理の中のIN命令で、ALレジスタにキーコードを入れれば良いです。すると、EAXにキーコードが入るので、OS側のc言語に値が渡るのです。

もう少し、具体的に説明すると、 キーボード入力を受け付けて、GUIからエミュレーターにデータ送るのは、GUIライブラリがすることです。私のコードの場合、OpenGLのfreeglutで実装しています。こんな感じです。

unsigned char keyboard_data;
void keyboard_callback(unsigned char key, int x, int y){
    keyboard_data = key;
    fprintf(stderr, "%d\n", key);
}

void GUI::ThreadProc(){
    glutKeyboardFunc(keyboard_callback);
}

こちらのkeyboard_dataに入力キーコードを送れば、エミュレータ側にデータを送れます。まだ実装できていないが、keyをキーボードのキーコードに直して、keyboard_dataに入れる処理も必要です。

あとは、以下のように、in8関数でkeyboard_dataを返してやると、in8命令を読んでいる関数の中で戻り値のkeyboard_dataをALレジスタに入れるようになります。

uint8_t keyboard::in8(uint16_t addr){
    extern unsigned char keyboard_data;
    switch(addr) {
        case 0x60: return keyboard_data;
    }
    return -1;
}

1.2 マウス

まだ、マウスは未実装です。多分、こうすれば、実装できるんじゃないか、と予想を書いておきます。 マウスもキーボードと同じように

void glutMouseFunc(void (*func)(int button, int state, int x, int y));

にコールバックされるmouse関数を渡して、その中で、ある処理をしてあげれば良いと思います。

その前にマウスは、どんなデーターを送るべきなのでしょうか?答えは、はりぼてOSに書いてます。簡単に説明すると、0xfaを送った後に3バイト送ります。その3バイトの1バイト目は、主にクリックしたかどうかがわかって、2バイト目は左右がわかります(xですね。)。また、3バイト目は、縦方向の座標が入ります。(y方向ですね。)

ある処理は、これらの0xfa 3バイトを送ってあげれば良いと思います。もちろん、keyboard_dataのようなmouse_dataバッファを作って、そこに入れてあげれば良いと思います。

glutでは、stateがマウスがクリックされているのかどうかを示すようです。

未実装なので、あくまで予想でした。

10 Tips

10.1 デバック

エミュレーター実装がバグっていて、その箇所を見つけたい場合は、qemuなどのエミュレーターと自作エミュレーターをどちらとも同じファイルを実行して、同じところでprintfして表示させてみます。例えば、構造体などをprintfします。qemuの正しい値と自作エミュレーターの値が違えば、そこらへんの処理がバグっているのかがわかるのです。まぁ、地味な作業なんだけど、目デバック?するよりも、こうするのが早いと思います。はりぼてOSの挙動がわかっていれば、qemuで実行しなくても、値がおかしいのかわかる人もいます。そうでない人は、次に紹介するqemuデバックをするのがいいでしょう。@yuyabu2さんの記事を参考にさせていただきました。ありがとうございます。

デバック方法

z_tools/qemu/Makefilegdbを使うように以下のように書き換えます。ファイル名などは便宜書き直してください。

QEMU     = qemu-system-i386
QEMU_ARGS   = -L . -m 32 -localtime -vga std -fda haribote.bin -monitor stdio -s -S
default:
    $(QEMU) $(QEMU_ARGS)

これで、make runすれば、黒い画面で止まります。 別ウィンドウを開いて、gdbを起動し、 target remote localhost:1234を実行して、接続します。

。。。まだ書いている途中。。。

※@yuyabu2さんの記事を参考にさせていただきました。ありがとうございます。

https://speakerdeck.com/yuyabu/gong-kai-yong

QEMUにGDBを繋げてhariboteOSをデバッグする方法 - Yabu.log

筆者がつまずいた点

eaxとalの関係

。。。まだ書いている途中。。。