自作OSに挑戦する日記 20日目
「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。
Chapter 20 「API」
今回やった内容
環境
- メインPC
- MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
- macOS Mojave 10.14
- 自作OSを動作させる環境
作業記録
ソース整理
かなり大規模なソース整理をしました。自分は勝手に改造していたところもあったので余計時間がかかりました…。
合わせてコンソールへ文字/文字列を出力するような関数を作りました。
1文字表示API
ソース整理の際に作ったコンソールへ文字を出力する関数を外部アプリから呼び出せるようにします。ただ、外部のアプリからOS内部の処理を呼び出すのは色々面倒なのでAPIを作ります。このAPIが窓口となってOS内部の処理を呼び出してくれます。
// 1文字表示 void console_putchar(struct CONSOLE *console, int char_id, char move){ char s[2]; s[0] = char_id; s[1] = 0; if(s[0] == 0x09){ // \t while(1){ putstr8_ref(console->sheet, console->cursor_x, console->cursor_y, COL8_FFFFFF, COL8_000000, " ", 1); console->cursor_x += 8; if(console->cursor_x == 8 + 384){ console_newline(console); } if(((console->cursor_x - 8) % 0x1f) == 0){ break; } } return; } if(s[0] == 0x0a){ // \n console_newline(console); return; } if(s[0] == 0x0d){ // do nothing return; } // 通常文字 putstr8_ref(console->sheet, console->cursor_x, console->cursor_y, COL8_FFFFFF, COL8_000000, s, 1); if(move != 0){ console->cursor_x += 8; if(console->cursor_x == 8 + 384){ console_newline(console); } } return; }
この関数を外部から呼べるようにします。外部アプリからは↑の関数は見えませんが、↓の関数が窓口の役割をしてくれることにより外部アプリからでもコンソールへの出力が出来るようになります…!
// nasmfunc.asm EXTERN console_putchar asm_console_putchar: PUSH 1 AND EAX, 0xff // ALレジスタの中身だけを取り出す PUSH EAX PUSH DWORD [0xfec] CALL console_putchar ADD ESP, 12 // スタックレジスタを12進める = スタックの要素を上から3つ捨てる RET
この状態で一度ビルドします。ビルドする際に吐かれる.mapファイルを見ることで、asm_console_putchar関数がどこに配置されたか確認することができます。番地がわかればもう勝ったも同然なので…
MOV AL, 'A' CALL 2*8:0xc31 RETF
コンソールへ出力するアプリ完成です!
ゴリ押しコードになったけどOSのAPIを呼び出して文字を出力できるようになった pic.twitter.com/za4whMk9ZJ
— ゆん (@yn0014) 2019年4月2日
Aだけでは満足できなかったのでごちうさHPのURLを出力するようにしてみました。とても簡単ですが初API完成です!
APIをどこからでも呼べるようにする
1文字APIが完成しました、今の状態だとasm_console_putcharの番地が変わるだけでAPIが動かなくなってしまう雑魚状態なので、どれだけ番地が変わろうと使い続けられるようにします。
本ではIDTを使ってこの問題を解決する方法が書かれていました。IDTで空いている番地にAPI関数を登録しておくと、INT 0x番地
でAPIを呼び出すことができるとのことでした。割り込みの時と同じですね!3日目くらいに INT
をたくさん使って文字を表示させていた時を思い出します…
// dectbl.c // APIをIDTに登録してどこからでも呼べるようにする set_gatedesc(idt + 0x40, (int) asm_haribote_api, 2 << 3, AR_INTGATE32);
アセンブリコードはINT命令を使うように変更します。
MOV AL, 'w' INT 0x40
IDTを経由して呼ばれた関数は割り込みとして扱われ割り込み禁止状態となるので、APIが呼ばれた直後に STI
をして割り込み許可します。そして RETF
で返す事が出来なくなるので IRETD
を使うように変えます。
IDTを経由するようにしたコードもちゃんと動いてくれました!
アプリケーション名を自由に設定できるように
アプリケーションを呼ぶためのコマンドが「myapp」で固定されているのは使いづらい…変えたい…という事で、アプリケーション名を自由に決めれるようにします。具体的には、アプリケーションのファイル名をコンソールに打ち込む事で実行できるようにします。
// console.c int command_app(struct CONSOLE *console, char command[]){ // ファイル名取り出し char file_name[18]; for(int idx = 0; idx < 18; ++ idx){ file_name[idx] = ' '; } int idx = 0; for(; idx < 13; ++ idx){ if(command[idx] <= ' ') break; file_name[idx] = command[idx]; } file_name[idx] = 0; // 実行ファイル存在確認 struct FILEINFO *file_info = file_search(file_name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); if(file_info == 0 && file_name[idx - 1] != '.'){ file_name[idx] = '.'; file_name[idx + 1] = 'o'; file_name[idx + 2] = 0; file_info = file_search(file_name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); } // 実行 if(file_info != 0){ struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; int *fat = (int *) memman_alloc_4k(memman, 4 * 2880); char *exec_addr = (char *) memman_alloc_4k(memman, file_info->size); int cursor_x = 8; // ファイル読み込み read_fat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); load_file(file_info->clustno, file_info->size, exec_addr, fat, (char *) (ADR_DISKIMG + 0x003e00)); // セグメント設定->実行 set_segmdesc(gdt + 1003, file_info->size - 1, (int) exec_addr, AR_CODE32_ER); far_call(0, 1003 * 8); // 後処理 memman_free_4k(memman, (int) fat, 4 * 2880); memman_free_4k(memman, (int) exec_addr, file_info->size); return 1; } return 0; }
ファイルの存在を確認する事が出来たら、セグメントを設定してあげてアプリケーションが配置されている番地まで far_call
してあげるようにします。
「gochiusa」コマンド完成です!(URLを表示するだけ…)
文字列表示API
20日目はもう少し進んで文字列表示APIも作るみたいです。
まずは1文字表示の時と同じようにCで関数を描いてあげます。
// 1行出力([0]文字が来るまで) void console_putstr(struct CONSOLE *console, char *str){ while(*str != 0){ console_putchar(console, *str, 1); ++ str; } return; } // 1行出力(サイズ指定) void console_putstr_with_size(struct CONSOLE *console, char *str, int size){ for(int idx = 0; idx < size; ++ idx){ console_putchar(console, *str, 1); ++ str; } return; }
そしてこれをIDTを経由して呼べるようにしてあげる…のではなく、今までとは違う形のAPIにします。何をするのかというと、APIの呼び出し窓口は1つだけにして、どの機能を実行するかはレジスタの内容で指定するようにします。(これでIDTを食いつぶす心配はなくなりますね…)
; API asm_haribote_api: STI PUSHAD ; 値保存用 PUSHAD ; 関数の引数用 CALL haribote_api ADD ESP, 32 POPAD IRETD
アセンブリ側のコードを変えてあげて…
void haribote_api(int edi, int esi, int edp, int esp, int ebx, int edx, int ecx, int eax){ struct CONSOLE *console = (struct CONSOLE *) *((int *) 0xfec); if(edx == 1){ // 1文字表示 console_putchar(console, eax & 0xff, 1); }else if(edx == 2){ // 1行表示 console_putstr(console, (char *) ebx); }else if(edx == 3){ console_putstr_with_size(console, (char *) ebx, ecx); } return; }
処理は全てC側の関数で行うようにします。これで複雑な分岐も書きやすくなったような気が…!
完成した新しいAPIですが、(本にもある通り)文字列表示機能は上手く動いてくれません…。21日目で直す事が出来るようです。ちらっと次を見てみるとコードセグメントレジスタがいけないだとか…。
まとめ
OS自作本2/3が完成しました〜!1日目と比べるとOS感が明らかに増していますね。あと10日、完成まで突っ走りたいと思います。
(作り始めてから3ヶ月が経ってしまったので「30日本やってます!」とは言えなくなってしまった…)