カーネルをブートするまで


OSの勉強のためにOSを実装していく事にしました。
電源投入してから何の処理もしないカーネルの処理を呼び出すところまで書いたのでまとめます。

今回作成したファイル:

実装した処理の概要はこんな感じです。

  1. FDの中身をメモリにコピーする (boot.nasm)
  2. カーネルの起動の準備をするコードにジャンプする (boot.nasm)
  3. PIC の割り込みを無効化する (loader.nasm)
  4. A20ラインを有効にする (loader.nasm)
  5. GDT を登録する (loader.nasm)
  6. IDT を登録する (loader.nasm)
  7. プロテクトモードになる (loader.nasm)
  8. パイプラインのフラッシュをする (loader.nasm)
  9. セグメント間ジャンプでCSレジスタにGDTのセレクタ値を設定する (loader.nasm)
  10. カーネル本体を1M以上の領域にコピーする (loader.nasm)
  11. カーネルにジャンプする (loader.nasm)
  12. 無限ループ (kernel.c)

動作はQEMUで確認しました。
QEMUは動作中に Ctr+Alt+2 を押すと、QEMUコマンドが入力出来ます。
QEMUのコマンドで "x/20i アドレス" を実行すると、アドレスのデータをディスアセンブルしたものが表示されます。
アドレスはベースアドレスを補正した値を指定しないとダメですね。当たり前ですが。

boot.nasm と loader.nasm はこんな感じです。
他のファイルはまとめてgithubここ に置かせてもらってます。

; boot.nasm

[org 0]
[bits 16]

jmp 0x07c0:start

%include "coroutine.inc"

start:
; セグメントレジスタの初期化
mov ax, cs
mov ds, ax
mov es, ax

; とりあえず文字列表示する
mov si, bgnMsg
call print

resetFdc:
mov ax, 0
mov dl, 0 ; Aドライブを指定して
int 0x13 ; FDコントローラをリセットする
jc resetFdc ; エラーが出たらやり直し

mov ax, loaderHead ; loaderHeadに読み込む
mov es, ax ; ES = loaderHead
mov bx, 0x0 ; ES:BX = loaderHead:0000
mov ah, 0x02 ; ES:BX を指定して
mov ch, 0 ; シリンダ0
mov al, 1 ; 1セクタ分
mov cl, 0x02 ; 読み込み開始セクタ2
mov dh, 0 ; ヘッド0
mov dl, 0 ; ドライブA

loopHead:
; FDを読み込む
mov ah, 0x02 ; ES:BX を指定して
mov al, 1 ; 1セクタ分
mov dl, 0 ; ドライブA

int 0x13 ; FDD読み込み

; 次のセクターを読むために変数を更新する
mov ax, es
add ax, 0x20 ; 512byte読み込むにあたり、リアルモードのアドレス指定だと +0x20
mov es, ax

add cl, 0x1
cmp cl, 19 ; セクターが1~18番まで
jb loopHead
mov cl, 1

; 必要になるまでコピーしない
;inc dh
;cmp dh, 2 ; ヘッドは0 or 1なので
;jb loopHead
;mov dh, 0

;inc ch
;cmp ch, 80 ; シリンダは各面に0~79番まである
;jb loopHead

loopEnd:

jmp loaderHead:0


bgnMsg db 'BOOT ', 0x0

times 510 - ($ - $$) db 0
       dw 0xaa55
; loader.nasm

[org 0]
[bits 16]
jmp short start

%include "coroutine.inc"

start:
mov ax, cs
mov ds, ax
mov es, ax
xor ax, ax
mov ss, ax

mov si, kmsg
call print

; AT互換機はCLIを呼ぶ前にこれをやらないといけないらしい
; と、30日でOS作る本に書いてあった

mov al, 0xff
out 0x21, al
nop
out 0xa1, al

; PIC の割り込みを無効化する
cli

; A20ラインを有効にする
call enableA20

; GDT を登録する
lgdt [gdtr]

; IDT を登録する
lidt [idtr]

; プロテクトモードになる
mov eax, cr0
or eax, 1
mov cr0, eax

; パイプラインのフラッシュをする
jmp pipeFlush

pipeFlush:
; セグメント間ジャンプでCSレジスタにGDTのセレクタ値を設定する
jmp dword codeSgmntSlctr:prtctModeBegin

prtctModeBegin:
[bits 32]
mov bx, dataSgmntSlctr
mov ds, bx
mov es, bx
mov fs, bx
mov gs, bx
mov ss, bx

mov esi, 0x0
kernelCopy:
mov eax, kernelFrom
add eax, esi
mov ebx, kernelTo
add ebx, esi
mov ecx, [eax]
mov [ebx], ecx
add esi, 0x2
cmp esi, kernelSize
jbe kernelCopy

jmp dword codeSgmntSlctr:kernelTo



[bits 16]
;<defun enableA20>
; A20 ラインを有効にする
enableA20:
call kbdclr
mov al, 0xd1
out 0x64, al ; これからコマンド送信をすると8042に伝える
call kbdclr
mov al, 0xdf
out 0x60, al ; A20ラインを有効にする
call kbdclr
mov al, 0xff
out 0x64, al
call kbdclr

ret

;<defun kbdclr>
;キーボードコントローラ8042の状態ポート 0x64 と入力バッファ0x60 を空にする
kbdclr:
waitkbdin: ; 8042の入力バッファが空になるまで読む
in al, 0x60 ; 0x60 を読む
test al, 0x2 ; バッファが空?
jnz waitkbdin

waitkbdout: ; 8042の状態ポートが空になるまで読みまくる
in al, 0x64 ; 0x64を読み込む
test al, 0x2 ; バッファが空?
jnz waitkbdout


ret

;</defun kbdclr>
;</defun enableA20>


kmsg db 'LOAD ', 0x0

; IDT
idtr dw 0 ; IDTのサイズ
dw 0, 0 ; IDTのアドレス

; GDT
gdtr:
dw gdtEnd - gdt - 1 ; GDT全体のサイズ
dd gdt + loaderHead * 0x10 ; GDTのアドレス

gdt:
dw 0, 0, 0, 0 ; NULLセレクタ

codeSgmntSlctr equ 0x08
dw 0xffff ; リミット 0xffff
dw 0x0 ; baseaddress 0~15
db 0x01 ; baseaddress 16~23
db 0x9a ; P:1 DPL:0 S:1 TYPE:10 Code non-conforming readable
db 0xcf ; G:1, D:1, limit 16~19: 0xf
db 0x0 ; baseaddress 24~31: 0x0
; base = 0x00010000

dataSgmntSlctr equ 0x10
dw 0xffff ; リミット 0xffff
dw 0x0 ; baseaddress 0~15
db 0x01 ; baseaddress 16~23
db 0x92 ; P:1 DPL:0 S:1 TYPE: Data, expand-up, writable
db 0xcf ; G:1 D:1 limit 16~19: 0xf
db 0x0 ; baseaddress 24~31: 0x0
; base = 0x00010000
gdtEnd:

times 512 - ($ -$$) db 0

参考図書:
30日でできる! OS自作入門
はじめて読む486
作りながら学ぶOSカーネル
アセンブリ言語の教科書
Binary Hacks
まるまるC MAGAZINE 2004年度版より7月号記事 オリジナルOSを作ろう!