自作OSに挑戦する日記 25日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 25 「ウィンドウ操作」
今回やった内容
- BEEP音再生
- 使える色を増やす
- 複数のコンソールウィンドウ
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
BEEP音再生
BEEP音を再生できるようにします。アプリからも使えるようにしたいのでAPIにも登録します。
BEEP音の制御にはPIT(Programmable Interval Timer)を使います。タイマを使えるようにした時以来ですが、なんでBEEP制御はPITを使ってやるんでしょうか…?ざっくり調べてみるとどうやら発振機が関係しているみたいです。BEEP音再生はシステムスピーカーを発振させて行うので、それを制御するためにPITに仕事が割り振られたのでしょうか(本当か?)。
PITを制御するためにはIN/OUT命令を使います。
/* api.c */ if(edx == 20){ // BEEP音再生 if(eax == 0){ int beep_conf = io_in8(0x61); io_out8(0x61, beep_conf & 0x0d); }else{ int beep_conf = 1193180000 / eax; io_out8(0x43, 0xb6); io_out8(0x42, beep_conf & 0xff); io_out8(0x42, beep_conf >> 8); beep_conf = io_in8(0x61); io_out8(0x61, (beep_conf | 0x03) & 0x0f); } }
EAXレジスタに0が入った状態でBEEP再生APIが呼ばれるとBEEP再生を停止するようになっています。それ以外は指定された周波数のBEEPを再生します。
早速試してみたいのですが、QEMUではBEEP音のエミュレートが出来ないため出来ないみたいです(実際できなかった)…。自由に使える実機が舞い込んできたら試してみようと思います。
使える色を増やす
今の動作モードでは最大256色まで使えるようにできるのですが、まだ16色しか登録していません。アプリからもウィンドウを扱えるようになったので、もう少し色を登録します。RGBそれぞれ6段階の階調を持たせた216色を新しく追加します。
/* graphic.c */ // +216色 unsigned char table_rgb_alpha[216 * 3]; for(int b = 0; b < 6; ++ b){ for(int g = 0; g < 6; ++ g){ for(int r = 0; r < 6; ++ r){ table_rgb_alpha[(r + g * 6 + b * 36) * 3 + 0] = r * 51; table_rgb_alpha[(r + g * 6 + b * 36) * 3 + 1] = g * 51; table_rgb_alpha[(r + g * 6 + b * 36) * 3 + 2] = b * 51; } } } set_palette(16, 231, table_rgb_alpha);
新しく増えた色は 16 + R + G * 6 + B * 36
をVRAMにセットすることで使うことができます(16は既に登録されていた数、RGBはそれぞれ0 ~ 6段階で設定)。
…ということでアプリを作って動かしてみました。
綺麗ですね!
これで使える色が格段に増えたのですが、もっと増やす方法があるみたいのなので実装します。以下の画像にように、異なる色を交互に配置することで擬似的に色を増やすことができます。それぞれの色について3色ずつ中間色は増えるので '(6 + 5 * 3) ^ 3` で計9261色表現できるようになります(!)。
…実際に動かしてみるとこのようになります。
綺麗🌈 pic.twitter.com/XKoV1jU8Rd
— ゆん (@yn0014) May 13, 2019
colorapp 1 と比べると色の移り変わりがとても綺麗です!
コンソールを増やす
コンソールを増やします。…今まではコンソールが1つしか存在しなかったのでアプリの複数起動が出来ませんでした。今の実装だと、コンソールが増えれば増えるほどアプリを複数起動することができます。
コンソールタスク・コンソールウィンドウを複数管理できるように
今まではコンソールタスクやウィンドウ描画のための領域を1つしか用意していなかったため、これを配列にして複数扱えるようにします。
/* bootpack.c */ unsigned char *buf_console[2]; struct TASK *task_console[2]; int *fifo_console[2]; for(int idx = 0; idx < 2; ++ idx){ sheet_console[idx] = sheet_alloc(sheet_ctl); buf_console[idx] = (unsigned char *) memman_alloc_4k(memman, 400 * 300); sheet_setbuf(sheet_console[idx], buf_console[idx], 400, 290, -1); task_console[idx] = task_alloc(); ~タスクに関しての設定など~ }
出力対応
タスクやウィンドウを増やしただけでは上手く動きません。コンソールへ出力を行う処理が決め打ちで行われていたので修正する必要があります。
タスクを管理する構造体でコンソールの情報を管理するようにし、出力はこの情報を使って行うことにします。データ読み込みのために必要なセグメント情報も、合わせてタスク構造体で管理するようにします。
/* bootpack.h */ struct TASK{ int sel, flags; int level, priority; struct FIFO32 fifo; struct TSS32 tss; struct CONSOLE *console; int ds_base; }; /* bootpack.c */ int *haribote_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax){ struct TASK *task = task_now(); int ds_base = task->ds_base; struct CONSOLE *console = task->console; ~~APIの処理~~ }
複数アプリを実行できるように
アプリを実行するときに設定するセグメント番号も決め打ちで行なっていたため修正します。TSSのセグメント番号を元に設定するようにしました。
/* console.c */ // セグメント設定 // アクセス権に0x60を足すとアプリケーション用のセグメントになる task->ds_base = (int) app_mem_addr; set_segmdesc(gdt + task->sel / 8 + 1000, file_info->size - 1, (int) exec_addr, AR_CODE32_ER + 0x60); set_segmdesc(gdt + task->sel / 8 + 2000, seg_size - 1, (int) app_mem_addr, AR_DATA32_RW + 0x60);
アプリ強制終了
アプリを強制終了させるための処理も決め打ちで行なっていたため修正します。key_to_window
には有効になっているタスクの情報が格納されていて、もしそのタスクがコンソールで、かつアプリが実行中なら強制終了処理を実行するようになっています。
/* bootpack.c */ // コンソール上のアプリケーションを強制終了する(backspace + shift) if(data == 256 + 0x0e && key_shift){ struct TASK *task = key_to_window->task; if(task != 0 && task->tss.ss0 != 0){ // 強制終了割り込み表示 struct CONSOLE *console = task->console; console_putstr(console, "\nKeyboard Interrupt\n"); // end_appに飛ぶようにする io_cli(); task->tss.eax = (int) &(task->tss.esp0); task->tss.eip = (int) end_app; io_sti(); continue; } }
…修正に修正を重ねて複数コンソール完成しました!
25日目!!!
— ゆん (@yn0014) May 13, 2019
OSっぽさがかなり増した…! pic.twitter.com/jxDibhzMCK
timerapp や colorapp の複数起動が出来ています!25日目完成!
入出力処理や強制終了処理もバグなく動いています。
まとめ
コンソールを増やすことが出来ました〜!表示しているウィンドウは2つだけですが、やろうと思えば何個でも立ち上げることが出来るようになっています。
OS自作本も終盤に近づいてきました。どこまで行けるか楽しみです!(改造はまたその後で…)
自作OSに挑戦する日記 24日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 24 「ウィンドウ操作」
今回やった内容
- ウィンドウ関連
- タイマAPI
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
実装回!!!
ウィンドウ関連
ウィンドウ切り替え1
Shift + CTRL で一番下にあるウィンドウを最前面表示するようにします。これに合わせてアプリ強制終了コマンドを Shift + BackSpace にしました。
/* bootpack.c */ // 一番下のウィンドウを一番上に引っ張り上げる (ctrl + shift) if(data == 256 + 0x1d && key_shift){ sheet_updown(sheet_ctl->sheets[1], sheet_ctl->top - 1); }
ウィンドウ切り替え2 & 移動
マウス左クリック時に登録されているウィンドウを上から見ていき、マウスの座標と重なっているものがあればそのウィンドウを最前面表示するようにします。
加えて、ウィンドウのタイトル部分をクリックしていた場合はウィンドウ移動モードに移行し、マウスの動きに合わせてウィンドウ座標を変更するようにします。
/* bootpack.c */ // 左クリック if((mdec.btn & 0x01) != 0){ if(win_move_x < 0){ // 上からウィンドウを見ていく… for(int sheet_idx = sheet_ctl->top - 1; sheet_idx > 0; -- sheet_idx){ struct SHEET *sheet = sheet_ctl->sheets[sheet_idx]; select_sheet = sheet; int x = mouse_x - sheet->vram_x; int y = mouse_y - sheet->vram_y; // ウィンドウをクリックした if(0 <= x && x < sheet->buf_width && 0 <= y && y < sheet->buf_height){ if(sheet->buf[sheet->buf_width * y + x] != sheet->col_inv){ // ウィンドウを上に持ち上げる sheet_updown(sheet, sheet_ctl->top - 1); } } }
ちゃんと動きます〜!
動いた〜🍃 pic.twitter.com/e8DwLUUrLU
— ゆん (@yn0014) May 7, 2019
ウィンドウをマウスで閉じる
ウィンドウ右上にある「×」ボタン(の表示)をクリックすることでアプリを強制終了できるようにします。強制終了コマンドと同じように EIP
を強制的に書き換えることで end_app
を実行するようにします。
/* bootpack.c */ // ウィンドウを閉じる(=強制終了) if(sheet->buf_width - 21 <= x && x < sheet->buf_width - 5 && 5 <= y && y < 19){ if((sheet->flags & 0x10) != 0){ struct CONSOLE *console = (struct CONSOLE *)*((int *) 0xfec); console_putstr(console, "\nExit\n"); io_cli(); task_console->tss.eax = (int) &(task_console->tss.esp0); task_console->tss.eip = (int) end_app; io_sti(); } }
キー入力対象ウィンドウを切り替える
今までは Tab
を押すことでミニ入力ウィンドウとコンソールを行ったり来たりしていたのですが、使い心地が悪くなってきたので(アプリも実行できるようになったので…)別の方法で入力対象ウィンドウを切り替えれるようにします。
key_to
変数で01を管理するのではなくkey_to_window
変数に入力対象ウィンドウ(シート)を持っておいて、この変数を元にに切り替えなどができるようにします。
ウィンドウクリックまたは Tab
を入力することで key_window_on / key_window_off
を呼び出すようにします。この関数内で入力切り替え処理やタイトル更新処理を行わせます。
/* bootpack.c */ // 指定ウィンドウを入力モードにする int key_window_on(struct SHEET *key_to_window, struct SHEET *sheet_window, int cursor_color, int cursor_x){ // タイトル更新 change_window_title(key_to_window, 1); // カーソル制御 if(key_to_window == sheet_window){ cursor_color = COL8_000000; }else{ if((key_to_window->flags & 0x20) != 0){ fifo32_put(&key_to_window->task->fifo, 3); } } return cursor_color; } // 指定ウィンドウを入力モード解除する int key_window_off(struct SHEET *key_to_window, struct SHEET *sheet_window, int cursor_color, int cursor_x){ // タイトル更新 change_window_title(key_to_window, 0); // カーソル制御 if(key_to_window == sheet_window){ cursor_color = -1; boxfill8(sheet_window->buf, sheet_window->buf_width, COL8_FFFFFF, cursor_x, 28, cursor_x + 7, 43); }else{ if((key_to_window->flags & 0x20) != 0){ fifo32_put(&key_to_window->task->fifo, 2); } } return cursor_color; }
タイマAPI
アプリからもタイマを使えるようにします。…基本的な実装は既にOS側で行われているので、APIはOS内の関数を代わりに読んであげる程度の動きしかしません。
また、TIMER構造体に flags_auto_cancel
を持たせることで取り残されたタイマを自動終了させれるようにします。これでアプリがタイマを取り消さずに終了してしまった場合でもOSに影響が出ません。安心。
/* api.c */ if(edx == 16){ // タイマ取得 reg[7] = (int) timer_alloc(); ((struct TIMER *) reg[7])->flags_auto_cancel = 1; return 0; } if(edx == 17){ // タイマ初期化 timer_init((struct TIMER *) ebx, &task->fifo, eax + 256); return 0; } if(edx == 18){ // タイマ時間設定 timer_set((struct TIMER *) ebx, eax); return 0; } if(edx == 19){ timer_free((struct TIMER *) ebx); return 0; }
…ということで完成した24日目がこちらです。
25日目! pic.twitter.com/kcEnOzZyok
— ゆん (@yn0014) May 9, 2019
タイマやウィンドウ操作などちゃんと動いています!
まとめ
前回に引き続き実装回でした。 実装回は楽に進めることが出来ますが、そろそろ新しい機能を実装したい気持ちが…!
自作OSに挑戦する日記 23日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 23 「グラフィックいろいろ」
今回やった内容
- malloc
- グラフィックいろいろ
- キー入力
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
malloc
アプリ用のmallocを作ります。
OS用のMEMMANで確保されたメモリにアプリからアクセスすることは出来ないので、アプリのデータセグメントを別のMEMMANで管理するようにします。
本では BIN2HRB
を使ってmalloc用の領域を指定する…とありますが、今の状態では使えないので har.ld
を変更して少し多めにアプリのセグメントを準備するようにしておきます。
/* api.c */ if(edx == 8){ // memman初期化 memman_init((struct MEMMAN *) (ebx + ds_base)); ecx &= 0xfffffff0; memman_free((struct MEMMAN *) (ebx + ds_base), eax, ecx); return 0; } if(edx == 9){ // malloc ecx = (ecx + 0x0f) & 0xfffffff0; reg[7] = memman_alloc((struct MEMMAN *) (ebx + ds_base), ecx); return 0; } if(edx == 10){ // mfree ecx = (ecx + 0x0f) & 0xfffffff0; memman_free((struct MEMMAN *) (ebx + ds_base), eax, ecx); return 0; }
/* api_link_obj.asm */ api_init_memman: ; api_init_memnan() PUSH EBX MOV EDX, 8 MOV EBX, [CS:0x0020] MOV EAX, EBX ADD EAX, 32*1204 MOV ECX, [CS:0x0000] SUB ECX, EAX INT 0x40 POP EBX RET api_malloc: ; api_malloc(int size) PUSH EBX MOV EDX, 9 MOV EBX, [CS:0x0020] MOV ECX, [ESP + 8] INT 0x40 POP EBX RET api_mfree: ; api_mfree(char *addr, int size) PUSH EBX MOV EDX, 10 MOV EBX, [CS:0x0020] MOV EAX, [ESP + 8] MOV ECX, [ESP + 12] INT 0x40 POP EBX RET
初期化する際にEAXに 32 * 1024
を足していますが、これはMEMMAN構造体用です。MEMMAN構造体を使うときにアドレスしか渡していませんが、こうすると指定したアドレス以降の必要な領域を自動で計算して使ってくれます。(最初ちょっと混乱しましたが…)
これでアプリ用のmallocが完成しました!
グラフィックいろいろ
これからとにかく実装するだけです。OSに既にある関数をAPIを通してアプリから呼び出せるようにします。
点を描画する
ESIにX座標、EDIにY座標、EAXに色番号を指定します。
/* api.c */ if(edx == 11){ // ウィンドウに点を描画する struct SHEET *sheet = (struct SHEET *) (ebx & 0xfffffffe); sheet->buf[sheet->buf_width * edi + esi] = eax; return 0; }
ウィンドウリフレッシュ
EAX/ECX/ESI/EDI に x0/y0/x1/y1 を指定します。
/* api.c */ if(edx == 12){ // ウィンドウリフレッシュ struct SHEET *sheet = (struct SHEET *) ebx; sheet_refresh(sheet, eax, ecx, esi, edi); return 0; }
リフレッシュ用APIが完成したので、既に実装済みのいくつかのグラフィックAPIを変更します。どのような変更かというと、EBXのSHEETアドレスが奇数ならリフレッシュを自動で行わないようにします。SHEETアドレスは必ず偶数になるのでその性質を使います。0xfffffffe
とANDを取ることで下位1ビットのみを取り出し、そのビットが1かどうかで偶奇を判定しています。
struct SHEET *sheet = (struct SHEET *) (ebx & 0xfffffffe); if(ebx & 0xfffffffe == 0){ sheet_refresh(sheet, eax, ecx, esi + 1, edi + 1); }
線を描画する
線を描画する関数は新しく作ります。x0/y0/x1/y1 から傾きを求めて、線を描画するようになっています。傾きを求める際、小さな数を擬似的に扱うため最初にXとYの値を 1024
倍しています。使うときは 10
ビット右にシフトすることで 1024
で割って元の数に戻しています。単純に 1000
倍するのではなくシフト演算を使うことで描画スピードを上げています。
/* graphic.c */ void drawline(struct SHEET *sheet, int x0, int y0, int x1, int y1, int color){ int dx = x1 - x0; int dy = y1 - y0; int x = x0 << 10; int y = y0 << 10; int len = 0; if(dx < 0) dx = -dx; if(dy < 0) dy = -dy; // 変化量設定 // (2 * (bool) - 1) => boolがtrueなら1, falseなら-1を返す if(dy <= dx){ len = dx + 1; dx = 1024 * (2 * (x0 < x1) - 1); dy = (y1 - y0 + (2 * (y0 <= y1) - 1) << 10) / len; }else{ len = dy + 1; dy = 1024 * (2 * (x0 < x1) - 1); dx = (x1 - x0 + (2 * (x0 <= x1) - 1) << 10) / len; } for(int cnt = 0; cnt < len; ++ cnt){ sheet->buf[sheet->buf_width * (y >> 10) + (x >> 10)] = color; x += dx; y += dy; } }
ウィンドウを閉じる
sheet_freeを呼びます!!楽!!
/* api.c */ if(edx == 14){ // ウィンドウを閉じる sheet_free((struct SHEET *) ebx); return 0; }
ちゃんと動きます!綺麗…
wow pic.twitter.com/JEnfbmbJ9K
— ゆん (@yn0014) 2019年5月5日
グラフィック関連APIはとにかく実装…という感じなので疾走感があります。
キー入力
アプリがキー入力を受け取れるようにします。実装は特に難しくなく、FIFOバッファをチェックしてデータが存在するなら返す…とするだけです。オプションでキー入力がされるまで待つか、1回だけFIFOバッファを確認してすぐ結果を返すかを選べるようにしています。もしキー入力までずっと待機する場合は、カーソルタイマーなどの処理を間時間に行うようにします。
/* api.c */ if(edx == 15){ // キー入力 while(1){ // FIFO確認 io_cli(); if(fifo32_status(&task->fifo) == 0){ if(eax != 0){ task_sleep(task); }else{ io_sti(); reg[7] = -1; return 0; } } // キー入力を待っている間は他のことをしたり… int data = fifo32_get(&task->fifo); io_sti(); if(data <= 1){ // タイマー timer_init(console->cursor_timer, &task->fifo, 1); timer_set(console->cursor_timer, 50); }else if(data == 2){ // カーソルON console->cursor_color = COL8_FFFFFF; }else if(data == 3){ // カーソルOFF console->cursor_color = -1; }else if(256 <= data && data <= 512){ // キー入力だった reg[7] = data - 256; return 0; } } }
前回の方法と同じように、スタックの値を書き換えることによって値を返しています。
…ということでアプリでキー入力を受け取れるようになったので簡単なゲーム(?)らしきものを作りました。23日目完成です!
23日目〜! pic.twitter.com/2y3bGrDluV
— ゆん (@yn0014) 2019年5月5日
(動画内でちょっとだけ出ていますが、強制終了したときにもウィンドウが閉じられるようになっています)
まとめ
グラフィック関連のAPIが充実してきました。キー入力も受け取れるになったので簡単なゲームならもう作れるようになった…ような気がします。多分作れます。
自作OS本もあと1週間になりました。終わったらどんな風に改造していくか…楽しみです。
自作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日ですが、本の厚さ的にはまだ結構あるような気が…楽しみです。
自作OSに挑戦する日記 21日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 21 「OSを守ろう」
今回やった内容
- 文字列表示API完成
- Cでアプリケーションを書けるように
- 例外
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
文字列表示API完成
文字列表示APIを完成させます。原因はコードセグメントの設定をしていなかったことで正しいメモリ番地を指定できなかったことにあるので、OS側でちゃんと設定してあげます。
/* console.c */ // アプリケーション読み込み char *exec_addr = (char *) memman_alloc_4k(memman, file_info->size); read_fat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); load_file(file_info->clustno, file_info->size, exec_addr, fat, (char *) (ADR_DISKIMG + 0x003e00)); // コードセグメントを保存しておく *((int *) 0xfe8) = (int) exec_addr;
実行部分からAPIへデータを直で渡す方法はないのでメモリ上に置いておきます。
/* api.c */ int *haribote_api(int edi, int esi, int edp, int esp, int ebx, int edx, int ecx, int eax){ // コードセグメント情報を取得する int cs_base = *((int *) 0xfe8); ~~~~~~~ }
これで文字列表示APIもちゃんと動作するようになりました〜。
Cでアプリケーションを書けるように
アセンブリも楽しいけどまだ慣れない…のでCでアプリケーションを書けるようにします。
コンソールへ何かを表示するためには INT 0x40
を実行する必要があるのですが、Cから実行することはできません。…なのでCとアセンブリの中間に立ってくれるような関数を作ります。
/* api_link_obj */ [BITS 32] GLOBAL api_putchar [SECTION .text] api_putchar: MOV EDX, 1 MOV AL, [ESP + 4] ; 引数1 INT 0x40 ; 1文字表示APIを呼ぶ RET
これで api_putchar
を経由してAPIが叩けるようになりました。Cからはこの関数を使うことでコンソールへの表示を行います。
早速Cでアプリケーションを書いてみます。
/* myapp.c */ void api_putchar(int c_code); void HariMain(void){ // Hello World! api_putchar('H'); api_putchar('e'); api_putchar('l'); api_putchar('l'); api_putchar('o'); api_putchar(' '); api_putchar('W'); api_putchar('o'); api_putchar('r'); api_putchar('l'); api_putchar('d'); api_putchar('!'); }
まだ1文字APIしか使えるようになっていないのでゴリ押しで出力させます。
このアプリケーションをコンパイルするには、api_link_obj.asm
と myapp.c
のオブジェクトファイルをそれぞれ作りリンクする…という2ステップを行う必要があります。
nasm -f bin -o api_link_obj.o api_link_obj.asm i386-elf-gcc -o myapp_c.o -c myapp.c i386-elf-gcc -m32 -O3 -march=i486 -nostdlib -fno-builtin -T har.ld -Wl,-Map=bootpack.map -o myapp.o api_link_obj.o myapp_c.o
myapp.c の実行ファイルである myapp.o
が完成しました!早速実行してみます。
ちゃんと動いてくれました〜!Cでアプリケーションが書けるようになったのはかなり嬉しいですね!(自作OS上でCで書かれた自作プログラムが動いている…)
例外
今のはりぼてOSで次のアプリケーションを実行すると、ディスク上のファイル情報が全く読めなくなってしまいます。(0x00102600はFAT情報が配置される先頭の番地)
void HariMain(void){ *((char *) 0x00102600) = 0; }
>>>crack<<< pic.twitter.com/QuHWIRepTe
— ゆん (@yn0014) 2019年4月25日
これはアプリケーションが本来アクセスすることができない場所へアクセスできてしまうことが原因です…。ということで悪意あるアプリケーションの実行からOSやディスクを守れるようにするために対策をしていきます。
まず、OSとアプリケーションでデータセグメントを分けるようにします。これでOS用のメモリ領域にアプリケーションはアクセスできなくなります。
// console.c char * exec_addr = (char *) memman_alloc_4k(memman, file_info->size); char *app_mem_addr = (char *) memman_alloc_4k(memman, 64 * 1024); ~~~~~ // アプリケーション用のセグメントを設定する set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER); set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) app_mem_addr, AR_DATA32_RW); start_app(0, 1003 * 8, 64 * 1024, 1004 * 8); ~~~~~ // 後処理 memman_free_4k(memman, (int) exec_addr, file_info->size); memman_free_4k(memman, (int) app_mem_addr, 64 * 1024);
64KB分メモリを確保して、その領域をアプリケーション用のデータセグメントとして与えます。また、アプリケーションを実行するための関数を作り、その中で切り替え設定を行います。
start_app
内では各種レジスタの中身をアプリ用に切り替え、fat-Callを行なっています。また、APIや割り込みの処理を行う関数の処理も変え、OSとアプリでセグメントを切り替えて実行できるようにします。(かなり大きな変更だった…)
これで、アプリケーションは自分のデータセグメントのみしかアクセスできなくなるため、上の「crack」アプリケーションは動かなくなりました👍
ここまでの対処法だと下のアプリケーションのメモリ操作が阻止できません。現在のHariboteOSはDSなどの値をアプリケーション自体が操作出来てしまう状態なので、対策が意味の意味がなくなっています。
…どうやって対策をするかというと、DSをアプリケーションから弄れないように(=OS用のセグメントを弄れないように)します。弄られてしまうなら弄れないようにすれば良い!…ということです。
x86には、アプリケーションの実行中にOS用のセグメントを代入しようとした時に例外を投げてくれるような機能があるとのことなので、ありがたく使わさせてもらうことにします。
アクセス権の設定
0x60を足すとアプリケーション用のセグメントとして設定される。楽ちん。
/* console.c */ // セグメント設定->実行 // アクセス権に0x60を足すとアプリケーション用のセグメントになる set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER + 0x60); set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int) app_mem_addr, AR_DATA32_RW + 0x60); start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
/* desctbl.c */ // 割り込みが発生したらasm_inthander_2*を呼び出すようにIDTを設定しているset_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 << 3, AR_INTGATE32); set_gatedesc(idt + 0x2c, (int) asm_inthandler2C, 2 << 3, AR_INTGATE32); // APIをIDTに登録してどこからでも呼べるようにする set_gatedesc(idt + 0x40, (int) asm_haribote_api, 2 << 3, AR_INTGATE32 + 0x60);
APIにアプリ終了の機能をつける
アプリ実行時にESPの値を保存するようにしているので、その情報を元にRETするようにします。
end_app: MOV ESP, [EAX] ; apiからTSS.esp0の番地が返ってくる POPAD RET
/* api.c */ int *haribote_api(int edi, int esi, int edp, int esp, int ebx, int edx, int ecx, int eax){ int cs_base = *((int *) 0xfe8); struct CONSOLE *console = (struct CONSOLE *) *((int *) 0xfec); struct TASK *task = task_now(); if(edx == 1){ // 1文字表示 console_putchar(console, eax & 0xff, 1); }else if(edx == 2){ // 1行表示 console_putstr(console, (char *) ebx + cs_base); }else if(edx == 3){ console_putstr_with_size(console, (char *) ebx + cs_base, ecx); }else if(edx == 4){ // アプリ終了 return &(task->tss.esp0); } return 0; }
他のコードもかなり変わりましたが長すぎるので載せれません…。アプリとOSのセグメントを切り替えて実行するようにしたり、例外割り込みが飛んできたときの処理を書き換えたりしました。
アプリ終了をAPIから行うようにしたので、「myapp」や「gochiusa」のコードが少し変わります。
/* myapp.c */ void harimain(void){ api_putchar('h'); api_putchar('e'); api_putchar('l'); api_putchar('l'); api_putchar('o'); ~~~~~~~~~~ api_end(); // ここ }
/* gochiusa.asm */ [BITS 32] start: MOV EDX, 2 MOV EBX, gochiusa_url INT 0x40 fin: MOV EDX, 4 // ここ INT 0x40 // ここ gochiusa_url: DB "www.gochiusa.com", 0
これで不正なアプリケーションの実行からOSを守れるようになりました!21日目完成!
例外〜〜! pic.twitter.com/NvblWCYV6B
— ゆん (@yn0014) April 25, 2019
まとめ
21日目の内容はセキリュティ要素が濃くかなり面白い回でした!
TSSやセグメント周りの知識が少し甘いような気がしたので復習します。