自作OSに挑戦する日記 22日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 22 「C言語でアプリケーションを作ろう」
今回やった内容
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
スタック例外・ゼロ除算例外
セグメント例外以外に、スタック・ゼロ除算例外も拾えるようにします。セグメント例外と同じようにCPUから例外割り込みが来るようになっているので、対応する番号をIDTに登録し、それを拾うような関数を実装します。
/* nasmfunc.asm */ asm_inthandler00: ; ゼロ除算例外 ~~~~ asm_inthandler0c: ; スタック例外 ~~~~
/* inthandler.c */ // ゼロ除算例外 int *inthandler00(int *esp){ struct CONSOLE *console = (struct CONSOLE *) *((int *) 0x0fec); struct TASK *task = task_now(); console_putstr(console, "\nINT 00 : Zero Division Exception.\n"); return &(task->tss.esp0); } // スタック例外 int *inthandler0c(int *esp){ struct CONSOLE *console = (struct CONSOLE *) *((int *) 0x0fec); struct TASK *task = task_now(); console_putstr(console, "\nINT 0C : Stack Exception.\n"); return &(task->tss.esp0); }
/* desctbl.c */ set_gatedesc(idt + 0x00, (int) asm_inthandler00, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 << 3, AR_INTGATE32);
実際に試してみます…が、スタック例外はQEMUだと上手く拾えないらしく表示されませんでした。実機でやればちゃんと拾われるみたいです。(本より)
例外が発生した場所を表示するようにする
今のままだとコードのどの部分で例外が発生しているのか分からないので、例外が発生した場所を一緒に表示するようにします。
例外が発生した場所やエラーコードはスタックに保存されています。Haribote OSは割り込みが発生した時、受け付ける関数でスタックの先頭アドレスを引数としてもらっています。このアドレス元にスタックの値を持って来れば例外が発生した場所などを表示することができます。
/* inthandler.c */ // ゼロ除算例外 int *inthandler00(int *esp){ struct CONSOLE *console = (struct CONSOLE *) *((int *) 0x0fec); struct TASK *task = task_now(); char view_str[40]; msprintf(view_str, "EIP : %x", esp[11]); console_putstr(console, "\nINT 00 : Zero Division Exception.\n"); console_putstr(console, view_str); return &(task->tss.esp0); }
表示されたアドレスを元に .map
ファイルを辿ることで、どのような実行がされた時の例外なのかを特定することが出来るようになります。
アプリ強制終了
今のHariboteOSはアプリの無限ループに耐えられません。これだとまずいので対策します。
// コンソール上のアプリケーションを強制終了する(ctl + shift) if(data == 256 + 0x1d && key_shift && task_console->tss.ss0 != 0){ // 強制終了割り込み表示 struct CONSOLE *console = (struct CONSOLE *)*((int *) 0xfec); console_putstr(console, "\nKeyboard Interrupt\n"); // end_appに飛ぶようにする io_cli(); task_console->tss.eax = (int) &(task_console->tss.esp0); task_console->tss.eip = (int) end_app; io_sti(); continue; }
アプリを実行していないときに強制終了コマンドが送信されても大丈夫なように、end_app
も少し改造しました。また、コンソール上に強制終了コマンドを書くことはできないのでメイン関数上に書きました。
これでControl + Shiftでアプリを強制終了することができます(end_appが実行される)。
文字列表示APIをCから使えるように
文字表示APIが実装できているので、文字列表示APIを同じように作ります。
/* myapp.c */ void HariMain(void){ api_putstr("Hello World!\n"); api_end_app(); }
…ですがこれでは動いてくれません。
ここで、自作OS初めの方で出てきた har.ld
をきちんと見直すことにします。 実行ファイルにはコード部分とデータ部分があり、文字列などのデータは全てデータ部分に置かれることになっているみたいです(本より)。現在のHariboteOSはデータ部分の設定をしていないので、実行した時にコードがデータを見つけられずに意図した動作をしてくれないのです。
/* har.ld */ OUTPUT_FORMAT("binary"); SECTIONS { .head 0x0 : { LONG(64 * 1024) /* 0 : stack+.data+heap の大きさ(4KBの倍数) */ LONG(0x69726148) /* 4 : シグネチャ "Hari" */ LONG(0) /* 8 : mmarea の大きさ(4KBの倍数) */ LONG(0x310000) /* 12 : スタック初期値&.data転送先 */ LONG(SIZEOF(.data)) /* 16 : .dataサイズ */ LONG(LOADADDR(.data)) /* 20 : .dataの初期値列のファイル位置 */ LONG(0xE9000000) /* 24 : 0xE9000000 */ LONG(HariMain - 0x20) /* 28 : エントリアドレス - 0x20 */ LONG(0) /* 32 : heap領域(malloc領域)開始アドレス */ } .text : { *(.text) } .data 0x310000 : AT ( ADDR(.text) + SIZEOF(.text) ) { *(.data) *(.rodata*) *(.bss) } /DISCARD/ : { *(.eh_frame) } }
har.ld
を確認すると、文字列などのデータが置かれるスタックの番地は 0x310000
から始まることが分かります。他にも har.ld
には実行ファイルとして必要な情報が書かれています。
現在、アプリの実行時に64KBのデータセグメントを与えていますが、これを変更してデータ部分を丸々データセグメントとして設定することにします。また、シグネチャを確認することで正しい実行ファイルかどうかを確かめてから実行するようにします。
/* console.c */ // 実行ファイルのシグネチャがHariなら実行 if(file_info->size >= 36 && mstrncmp(exec_addr + 4, "Hari", 4) == 0 && *exec_addr == 0x00){ // .hrb内のデータ情報を持ってくる int seg_size = *((int *) (exec_addr + 0x0000)); int esp = *((int *) (exec_addr + 0x000c)); int data_size = *((int *) (exec_addr + 0x0010)); int data_hrb_addr = *((int *) (exec_addr + 0x0014)); char *app_mem_addr = (char *) memman_alloc_4k(memman, seg_size); // セグメント設定 // アクセス権に0x60を足すとアプリケーション用のセグメントになる *((int *) 0xfe8) = (int) app_mem_addr; set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER + 0x60); set_segmdesc(gdt + 1004, seg_size - 1, (int) app_mem_addr, AR_DATA32_RW + 0x60); // データをデータセグメントに移す for(int idx = 0; idx < data_size; ++ idx){ app_mem_addr[esp + idx] = exec_addr[data_hrb_addr + idx]; } // 実行 start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0)); memman_free_4k(memman, (int) app_mem_addr, seg_size); }else{ console_putstr(console, ".hrb File Format Error!\n"); }
これで、アプリがデータ部分を参照できるようになりました。
ちゃんと api_putstr
が動いています〜!正しい実行ファイルでないファイルを実行しようとした時も、エラーを出して弾くようになっています。
グラフィック関連のAPIを実装
グラフィック周りのAPIを整えます。特に新しく実装するとかではなく、Cで既に作った関数を呼び出してあげるだけです。
ウィンドウ表示・文字描画・四角形描画APIを実装しました。
/* api.c */ int *reg = &eax + 1; if(edx == 5){ // ウィンドウ生成 struct SHEET *sheet = sheet_alloc(sheet_ctl); sheet_setbuf(sheet, (unsigned char *) ebx + ds_base, esi, edi, eax); make_window((unsigned char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0); sheet_slide(sheet, 400, 100); sheet_updown(sheet, 3); reg[7] = (int) sheet; } else if(edx == 6){ // ウィンドウに文字列を描画 struct SHEET *sheet = (struct SHEET *) ebx; char s[40]; msprintf(s, "%d, %d, %d", ebx, sheet->buf_width, sheet->buf_height); console_putstr(console, s); putstr8(sheet->buf, sheet->buf_width, esi, edi, eax, (char *)(ebp + ds_base)); sheet_refresh(sheet, esi, edi, esi + ecx * 8, edi + 16); } else if(edx == 7){ // ウィンドウに四角形を描画 struct SHEET *sheet = (struct SHEET *) ebx; boxfill8(sheet->buf, sheet->buf_width, ebp, eax, ecx, esi, edi); sheet_refresh(sheet, eax, ecx, esi + 1, edi + 1); }
ウィンドウ生成APIからアドレスが帰ってくる部分ですが、これはスタックの値を強制的に弄ることで実装しています。
APIが実行された時、スタックは [レジスタ保存用PUSH][API用PUSH]
の順で積まれています。[API用PUSH]
の一番下にあるEAXのアドレスを参照し、その次のアドレスを計算することで [レジスタ保存用PUSH]
のスタックの値を参照できるようにします。また、そこからEAXの値が置かれている番地にアクセスすることで、API呼び出し側に値を返すようになっています。
少し雑ですが、自分が理解するために作ったものを貼っておきます。
3つのグラフィック関連のAPIが完成したので動かしてみます…
22日目完成〜! pic.twitter.com/fu7UdKqZcP
— ゆん (@yn0014) 2019年4月30日
できました〜!22日目完成です。
まとめ
グラフィック関連APIの実装はかなり勉強になりました。スタックアドレス動き方やPUSHADなどの命令について理解できました〜。
22日目が終わり残りはあと8日ですが、本の厚さ的にはまだ結構あるような気が…楽しみです。