OSを作ってみよう

OSという言葉は知っているだろうか。そのOSを今回は作ってみようという企画である。

OSを作るには、まず以下のようなものが必要となる。
ブートローダー
カーネル
ドライバ
これらが必要となる。

まずは第一回ということで、ブートローダーを作ってみた。
(数ヶ月かかった。)

default rel
section .data
    ImageHandle dq 0    ;住所の形式は64bitなのでdqを使い、UTF-16の文字の場合は一文字が16bitなのでdw
    SystemTable dq 0
        ConOut dq 0
            OutputString dq 0
        ConIn dq 0
            ReadKey dq 0
        RuntimeServices dq 0
            shutdown dq 0
        BootServices dq 0
            uefiexit dq 0
            getmemorymap dq 0
            
            HandleProtocol dq 0
            AllocatePool dq 0
            FreePool dq 0

            ;getmemorymapの返り値

            align 8 ;8バイト境界に整列。
            mapsize dq 4096
            memorymap times 4096 db 0   ;UEFIの仕様上、getmemorymapはこのようにしてバッファを用意しておく必要がある。
            mapkey dq 0
            DescriptorSize dq 0
            DescriptorVersion dd 0

        inputkey dq 0   ;文字データ自体はdwでいいのだが、readkeyは他のデータも2byte分書き込むのでオーバーフロー対策でddに、そして保険としての意味も込めてdqに。
        tempkey dw 0
    ;文字列の後ろには0を絶対つける(db限定)。改行は13,10で終了は0。
    msg dw __utf16__('Hello!'), 13,10, 0
    bootmsg dw __utf16__('Do you want to boot?'), 13, 10, __utf16__('y/n'), 13, 10, 0
    Fatal_error_msg dw __utf16__('Fatal_error_detected:...Restart...'), 13, 10, 0
    Reboot_Failure dw __utf16__('Restart_failed:...Force_stop...'), 13, 10, 0
    Debug_msg dw __utf16__('Debug'), 13, 10, 0
    register dw 0
section .text
    global _start
    _start:
        ;最初はUEFIがいくつかのレジスタにUEFI命令のアドレスが入っている(rcx=ImageHandle(自分のプログラムの識別子)rdx=SystemTable(サービスの参照が入っている))
        
        sub rsp, 40 ;スタックを調整
        xor r15, r15

        ;レジスタからメモリへ保存
        mov [ImageHandle], rcx
        mov [SystemTable], rdx

        mov rcx, [rdx + 64] ;SystemTableの64byte先には画面出力のConOutの住所が入っている
        mov [ConOut], rcx
        mov rax, [rcx + 8]  ;ConOutの8byte先に文字列のOutputStringが入っている
        mov [OutputString], rax
        lea rdx, [msg]    ;rdxに文字列のデータを入れる
        
        call rax    ;今回の引数:rcx SystemTableの住所(固定) rdx 文字列データ

        ;ここでブートするかを聞く文を表示

        global judge_msg
        judge_msg:

        lea rdx, [bootmsg]
        mov rcx, [ConOut]   ;rcxはUEFIが書き換えることができる揮発性レジスタ故保険でもう一度mov。
        mov rax, [OutputString]

        call rax

        cmp r15, 0   ;r15は0のままなのでこれで初回かどうかを判定できる。初回なら0、二回目以降なら0以外。
        jnz setupskip

        ;他のツールの住所を指定しておく
        mov r8, [SystemTable]

        mov rcx, [r8 + 48]
        mov [ConIn], rcx
        mov rax, [rcx + 8]
        mov [ReadKey], rax
        
        mov rcx, [r8 + 88]
        mov [RuntimeServices], rcx
        mov rax, [rcx + 104]
        mov [shutdown], rax

        mov rcx, [r8 + 96]
        mov [BootServices], rcx
        mov rax, [rcx + 232]
        mov [uefiexit], rax

        mov rcx, [r8 + 96]
        mov rax, [rcx + 32]
        mov [getmemorymap], rax

        setupskip:

        jmp _bootjudge  ;ブートしたいか判定するプロセスへ

    ret

    global _bootjudge
    _bootjudge:

        key:

        mov rcx, [ConIn]
        xor edx, edx
        mov [inputkey], edx
        lea rdx, [inputkey]
        mov rax, [ReadKey]

        call rax

        test rax, rax   ;ここでのraxは成功したかどうかがわかる。testはそれを確認できる。
        jz _success

        mov r10, 0x8000000000000006 ;二度手間の理由は64bit数値とレジスタを直接cmpできないx86_64規則のため。
        cmp rax, r10 ;何も入力されていない場合は0x8000000000000006となるのでこれだった場合はkeyに。
        je key

        jmp Fatal_error_handler ;これ以外ということは致命的なエラーなので再起動、失敗したらシャットダウン、それでもダメならhlt(CPU強制停止)。

        _success:

         movzx ecx, word [inputkey + 2]  ;inputkeyに返り値が入っている。
        or ecx, 0x20  ;これで大文字を小文字に強制的に変換(右から6番目のビットを強制的にオンにする→2^6=32=16 x '2' +1 x '0' =0x20)

        cmp ecx, 'y'
        je _exituefi

        cmp ecx, 'n'
        je _shutdown

        jmp judge_msg   ;cmpでyおよびnでもなかった場合はy,n以外だったということなのでjudge_msg:に戻る。

        add rsp, 40
    ret

    global _shutdown
    _shutdown:
        ;(ここに来る際にはrspはすでに40なのでスタックの確保は省いてよし。)
        mov rcx, 2  ;ここでどの操作をするかを選択
        xor rdx, rdx    ;基本的にはほぼ必要ないが追加データを入れることもできる(rdx r8 r9に入れる)
        xor r8, r8
        xor r9, r9
        mov rax, [shutdown]
        call rax
        add rsp, 40 ;失敗した際の後片付け
    ret

    global _exituefi
    _exituefi:
        sub rsp, 16 ;第五引数(実はrspの空き容量は1バイト分だけ少なくなってしまう!)も使うため48以上は必要だが8+48では16の倍数にならない。そしてこのプロシージャに来る時はrspは40なので差分の16を引けばOK。
        lea rcx, [rel mapsize]
        lea rdx, [rel memorymap]
        lea r8, [rel mapkey]
        lea r9, [rel DescriptorSize]
        lea rax, [rel DescriptorVersion]
        mov [rsp + 32], rax   ;第五引数はスタックに入れる
        mov rax, [getmemorymap]
        call rax

        mov rcx, 0x8000000000000002
        cmp rax, rcx
        je debug

        jmp skip
        debug:
        call _debug
        cli 
        halt:
        hlt
        jmp halt
        skip:

        mov rcx, [ImageHandle]
        mov rdx, [mapkey]
        mov rax, [uefiexit]
        call rax

        add rsp, 56
    ret

;ここからは通常は使われないプロシージャ

    global Fatal_error_handler  ;致命的なエラーが発生した際のハンドラー
    Fatal_error_handler:
        mov rcx, [ConOut]
        mov rax, [OutputString]
        lea rdx, [Fatal_error_msg]
        call rax
        mov rcx, 5000000
        mov r10, [BootServices]
        mov rax, [r10 + 168]
        call rax
        
        mov rcx, 0  ;これによって再起動がされる。
        mov rdx, 0x8000000000000007 ;エラーコードはわからんがとりあえずDEVICES ERRORで。このプログラム自体が簡素な分起きるエラーも少ない。
        xor r8, r8
        xor r9, r9
        mov rax, [shutdown]
        call rax
        ;再起動すら失敗したガチでやばいケースの場合
        mov rcx, [ConOut]
        mov rax, [OutputString]
        lea rdx, [Reboot_Failure]
        call rax
        mov rcx, 5000000
        mov r10, [BootServices]
        mov rax, [r10 + 168]
        call rax
        add rsp, 40

        cli
        halt2:
        hlt
        jmp halt2

        global _debug
        _debug:
        push rax
        mov rcx, [ConOut]
        mov rax, [OutputString]
        lea rdx, [Debug_msg]
        call rax
        pop rax
        ret

こんなところだろう。
まあまだ荒削りの部分もあると思うが、これからも頑張っていきたい。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です