30日目のはりぼてOSが動作するx86エミュレータを実装してみた。(感想)


自作OS Advent Calendar 2021の5日目

x86エミュレータのソースコードを公開していますので、見てくれると嬉しいです…

去年からたま~にプライベートリポジトリで作っていたんですけど、新たなリポジトリで1から開発を始めました。だから、末尾に2がついてます。世の中には、偉大な先人たちによる多くのx86エミュレータが存在します。ですが、車輪の再発明を行うことにしました。夢があって、Jit化してサクサクはりぼてOSを動かしたいんです。そのために取り敢えずx86エミュレータを開発してみようかなと。ここからは、だらだらと感想文を書いていきます。

目次

1.開発したx86エミュレータについて
2.x86エミュレータを実装する際に注意したいこと
3.僕なりの開発に対する向き合い方
4.今後やりたいこと
5.エミュレータを実装する際に読むべき本
6.終わりに
7.実装したx86エミュレータのリポジトリ

1 開発したx86エミュレータについて

30日でできる! OS自作入門で実装するOSを自作x86エミュレータで動かしています。30日目のアプリが音アプリ以外動きます。

30日でできる! OS自作入門のbballアプリが動作した時は、感動しました。


アプリがいっぱい動きます。timerをエミュレートしているので、マルチタスクをOSが実装できるようになっています。

gviewアプリが動作した時は嬉しかった。。。
※gviewアプリとは、はりぼてOSで登場するjpg形式の画像を表示するアプリ

せっかくなので、手元にある画像も表示させてみた。

※マリオは僕の手元にあるスクショで、それをフロッピーディスクイメージに付け足したものです。おおーとなりました。(小並感)

特徴

1.SDL2.0という互換性の高いGUIライブラリを使用(クロスプラットフォームのはず。Macでしか動作確認していない。)
2.リアルモードから起動(ただし、現時点ではBIOSはエミュレートされているだけで、リアルモードからプロテクトモードに移行するプログラムしか実行できません。いずれ変更したいです。)
3.2種類の解像度をサポート、リアルモードでBIOSのサービスにより動的に変更可能(320x200と640x400)
4.30日でできる! OS自作入門 川合 秀実の1日目から実行可能で、30日目のharib27dまで動作可能(ただしアプリケーションの保護はまだ未実装です。CPUはアプリケーションの暴走を見逃します。)

特徴とまでは言えないのですが、命令をクラス化した話をここでしようと思います。機械語命令クラスの親クラスを用意して、親クラスへのポインタ配列に子クラスを格納するというやり方を採用してます。一部簡略化して、C++で解説します。

class MovR32Imm32:public Instruction{
    public: 
        MovR32Imm32(std::string code_name);
        void Run();
};

上のようにInstructionクラスを継承した機械語命令クラスを用意します。Cpuクラスの方では、Instructionクラスのポインタ配列を用意します。

#define INSTRUCTION_SIZE 256
class Cpu{
   private: 
      省略
      Instruction* instructions[INSTRUCTION_SIZE];
      省略
   public:
      省略
};

Cpuのメイン関数で、

void Cpu::Run(省略){
    uint8_t op_code;
    省略
    this->instructions[op_code]->Run();
    return;
}

と書けば、機械語命令が実行されます。if文とかswitch文で書くよりは綺麗に書けるので、お勧めです。

問題点

  • 音関連アプリは全く動かない
  • 特権レベルを結構適当に実装
  • 起動が遅い

僕はカラオケが苦手です。60点程度の実力です。こんな下手な人見たことないと言われたことがあります。なので、音関連は実装ができないんです。音は分かりません。ドレミファソラシド分かりますか?僕は分かりません。あ、特権レベルの実装が結構適当です。アプリケーションが暴走しても、CPUは検知しません。ここは今後実装予定です。起動が遅い理由は、30日でできる! OS自作入門の29日目のファイル圧縮の章で起動が遅くなりました。どの機械語命令のせいだったか、正確には覚えていませんが、forループだと愚直に実装できる機械語命令だった気がします。Jit化すると、物凄く速くなりそうです。
あ!!Macでこのエミュレータを動かそうとしている人は、ファンクションキーを利用できるように設定してくださいね。でないと、エミュレータにファンクションキーが伝わりません。

2 x86エミュレータを実装する際に注意したいこと

異なる特権レベル間でのスタック切り替え

異なる特権レベル間でのJmp命令やCALL命令には注意が必要です。この場合、スタックポインタやスタックセグメントの入れ替えをCPU側で行う必要があります。実はx86では、TSS領域を利用します。TSSはマルチタスクのために存在しているというイメージがあるかもしれませんが、スタックの入れ替えにも利用されます。同じTSSを維持した状態での異なるセグメント間移動でも、TSSは利用されてます。意外とここを知らない人は多いのではないでしょうか?30日でできる! OS自作入門のアプリ開発の章に突入した時は思い出してください。

struct _Tss{
    uint32_t backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
    uint32_t eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
    uint32_t es, cs, ss, ds, fs, gs;
    uint32_t ldtr, iomap;
}__attribute__((__packed__));

僕のリポジトリから、TSS構造体を引っ張ってきました。上を見ると、esp0やss0やesp2という変数があります。espとssの後の番号の意味は、ジャンプ先の特権レベルの番号を表しています。アプリケーションからシステムに制御が移るときに、システムの特権レベルiに該当するespiとssiをESPとSSにロードしていきます。 3がない理由ですが、必要ないからです。システムがクラッシュしないためのスタック領域なので、特権レベル3用の変数は要りません。アプリケーションでスタックを使い尽くした状態で、OSに制御が移ると、OSはクラッシュします。ちなみに、30日でできる! OS自作入門ではesp0とss0しか利用しません。特権レベル0と3しか利用しないからですね。

※同じ特権レベル間での移動はCSとEIPの入れ替えを行うだけなので、実装は簡単です。30日でできる! OS自作入門ではアプリ開発の章に進むまでは、気にしなくていいです。むしろ考えない方が挫折せずに済むし、継続して開発ができます。細かく考えると独学で開発を進める性質上、挫折します。

3 僕なりの開発に対する向き合い方

挫折しないために

何かを作りたい時、継続して開発を行っていく必要があります。当たり前の話ではありますが、これが意外と難しいです。あれもこれも実装しなきゃと考えているから、継続できないんだと思います。だから、特定の入力の時だけは、プログラムを停止するようにして、先を実装するというのは有効な手段ではないかと思います。自分の中で実装したい機能の優先度があると思うので、優先度の高いものから実装しましょう。1点集中です。高解像度をサポートする優先度は僕にとって、低かったので、30日目まで後回しにしました。あの時はとにかく動くものを作りたかったので、高解像度だろうがそうでなかろうがどうでも良かったんです。継続するためのコツをコードで説明してみます。

「まずはmain関数書かないと何も始まらないでしょ。main関数書くか。」

int main(){
   return 0;
}

->これができる時点で、自分を褒めてください。ほとんどの人は、あれこれ細かく考えすぎます。完璧主義にこだわり、結局main関数すら書かずに挫折します。だから、main関数を書けたら、勝ち組です。main関数すら書かずに、やめていく人が9割です。(偏見)

「コンパイルできた。まあ当たり前か。次はバイナリファイルを読み込めるようにするか。ファイル名は取り敢えず、固定でいいか。」

int main(){
   char* file_name = "haribote.img";//フロッピーディスクイメージファイル名
   FILE* fp = fopen(file_name, "rb");
   if(fp==NULL){
      fprintf(stderr, "ファイルが存在しません。");
      exit(1);
   }
   fclose(fp);
   return 0;
}

「これをコンパイルしよっと。お、コンパイルできた。よしよし。次はファイルの中身をバッファに展開してみるか。まずはディスクの先頭の512バイトを読み込めるようにしてみよう。」

#define IPL_SIZE 512
int main(){
   char* file_name = "haribote.img";//フロッピーディスクイメージファイル名
   FILE* fp = fopen(file_name, "rb");
   uint8_t *buff = NULL;
   buff = (uint8_t*)malloc(IPL_SIZE);
   if(fp==NULL){
      fprintf(stderr, "ファイルが存在しません。");
      exit(1);
   }
   fread(buff, 1, IPL_SIZE, fp);
   fprintf(stderr, "buff[0]=0x%02X, buff[1]=0x%02X\n", buff[0], buff[1]);
   free(buff);
   fclose(fp);
   return 0;
}

「これをコンパイルしよっと。お、どうやらbuffの先頭の2バイトの中身はディスクイメージと一致してそうだ。よしよしファイルの中身を読み込めるようになったわけだな。次は、CPU構造体を定義しよう。」

#define IPL_SIZE 512
struct Cpu{
   uint8_t op_code;
};

int main(){
   char* file_name = "haribote.img";//フロッピーディスクイメージファイル名
   FILE* fp = fopen(file_name, "rb");
   uint8_t *buff = NULL;
   if(fp==NULL){
      fprintf(stderr, "ファイルが存在しません。");
      exit(1);
   }
   buff = (uint8_t*)malloc(IPL_SIZE);
   fread(buff, 1, IPL_SIZE, fp);
   fprintf(stderr, "buff[0]=0x%02X, buff[1]=0x%02X\n", buff[0], buff[1]);
   fclose(fp);
   free(buff);
   return 0;
}

「コンパイルしたらどうなるかな?お、コンパイルできた。まあそりゃあそうか。次は、Cpu構造体のop_code変数にbuff[0]の値を入れてみるか。」

こんな感じで淡々と少しずつ機能を拡張していきます。作ろうか迷ったら、取り敢えずmain関数を書くといいです。

4 今後やりたいこと

  • x86用JITコンパイラ
  • フロッピーディスクコントローラのエミュレート
  • FreeDOS(16bit)を動かす
  • wasm化
  • 自作OSを自作x86エミュレータ上で動かす
  • リファクタリング

x86エミュレータ用のJITコンパイラを作りたいです。これが何よりもやりたいことです…先月、Jitファミコンエミュレータを開発してみました。ズルしたJitファミコンエミュレータです。1フレームしか描画しないので、ズルしてます。32bit機械語を利用して、Jit化したために、手元に32bitSDLライブラリがなかったんです。1フレーム用意して、別64bitGUIプログラムに、そのフレームを渡して描画するという仕組みです。今後x86エミュレータをJIT化したく、その為の練習としてファミコンエミュレータをJIT化してみました。けど、x86エミュレータをJit化するためには、x64のアセンブリ言語に慣れないと作れなそうです…先は長い…フロッピーディスクについては、以前にフロッピーディスクドライバを実装した経験があるので、多分なんとかなると思います。wasm化できたら、やりたいことがあるんです。cssを利用して、レトロなパソコンを描画して、ディスプレイ部分に僕のエミュレータを埋め込みたいです。そして、フロッピーディスクが動いているときは、「ガチャガチャ」と音が鳴るみたいなやつ作りたいです。
リファクタリングしたい理由は、察してください。

5 エミュレータを実装する際に読むべき本

よくありがちな書籍しか並ばないです。ごめんなさい。どの記事でも登場するということは、それだけ価値のある本だと言えるかもしれません。

はじめて読む486―32ビットコンピュータをやさしく語る 蒲地 輝尚
->いきなりintelの仕様書を読んでも挫折します。まずはx86のことを勉強したいなら、これを読むといいです。これなぜか絶版なんですよね。なんで絶版なんでしょうか? 中古もしくは電子媒体のみです。僕は運よく中古が手に入りました。

30日でできる! OS自作入門 川合 秀実
->30日間かけて、インクリメンタルにOSを開発していく本ですね。1週目はOS自作に2週目はx86エミュレータ自作に利用できます。つまり、実質半額です。この本がきっかけで、低レイヤに興味を持ちました。当時はワクワクして読んでいました。初めてポケモンというゲームを遊んだ時のワクワク感と似てます。

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

->この本はx86エミュレータ入門書です。この本はエミュレータ自作してみようか迷っている人の背中を押してくれる本です。この本、めちゃくちゃ分かりやすいのでお勧めです。意図的なのかどうか知りませんが、この本はページ数が少ないです。なので、読破しやすくまずはこちらの本を読むといいかもしれません。こちらの本もワクワクして読みました。

ゼロからのOS自作入門 内田 公太
->今回のエミュレータ自作と関係ないのですが、64bit版エミュレータを作るには打ってつけの本ではないかと思います。私はまだ読んだことありませんが、この本は1週目はOS自作に、2週目はエミュレータ作りに利用すると良いかもです。UEFI BIOSが絡んでくるので、いきなりOSの先頭から起動するようにしたほうがいいかもです。だから、OSのファイルフォーマットを解析して、先頭の番地を求め、そこから起動すれば、多分動くのではないでしょうか?ということで、こちらの本も2周できるので実質半額です。

6 終わりに

自分のことを少し書こうと思います。プログラミングはもともと大嫌いでした。学部生時代(情報系)は、ポインタを見るだけで吐きそうになってました。私の大学のレベルは高くなく、OSの講義もコマンド操作(cdとかlsとかgrepを使えるようになろう)が中心で、CPUの講義は座学です。コンパイラの講義はありませんでした。CPU自作といった実験もありません。僕にとって低レイヤは魔法の世界。ですが、川合さんや内田さんや蒲地さんの本と出会ったことで、こんな面白い世界があるんだと!!と知りました。全然強くはなっていないですし、強くなる気配なんて1ミリもないんですが、少なくともプログラミングは楽しいなと思えるようになりました。インターネットがある時代に生まれてよかったなと思います。。。インターネットがなければ、こんな世界を知ることもなかったでしょうし、田舎でゲームを沢山してひっそり暮らしていたことでしょう。欲を言えば、もっと若い時に知りたかったです。17とか18歳とかでこの世界と出会えていたらと思うと、ちょっと悲しくなります。(この時、精神病で苦しんでいたので、それどころではありませんでしたが。最近、ようやく少しずつ良くなってきてはいます。)とにかく、こんな面白い世界があるなんてことを教えてくれて、ありがとうございます。

7 実装したx86エミュレータのリポジトリ