自作OSに挑戦する日記 20日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 20 「API

今回やった内容

  • ソース整理
  • 1文字表示API
  • 文字列表示API

環境

作業記録

ソース整理

 かなり大規模なソース整理をしました。自分は勝手に改造していたところもあったので余計時間がかかりました…。
 合わせてコンソールへ文字/文字列を出力するような関数を作りました。

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

 コンソールへ出力するアプリ完成です!

 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 してあげるようにします。

f:id:guguru0014:20190423214348p:plain
gochiusaコマンド

 「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日本やってます!」とは言えなくなってしまった…)