カーネル制作体験

低レベル層に興味がある人にはインテルの、ここ(日本語資料)とか ここ(英語資料)とかにあるソフトウェア開マニュアルは非常に面白い読み物だと思う。でも実際に試してみないと分かった気がしないなーと言うことでカーネルを作ってみることにした。

とりあえず最小限の何の役にも立たないような物でいい。ややこしげなメモリアクセス、割り込み、特権レベル、タスクスイッチ、ページングとかをごくごく小規模で気軽にいじくり回せるようなのが良い。

作りながら学ぶOSカーネル保護モードプログラミングの基本と実践

作りながら学ぶOSカーネル保護モードプログラミングの基本と実践

と思ってたらうってつけの本が有ったので購入(笑) GDTとかIDTを最低限で作る方法が良くわからなかったけど丁度良い感じの解説が載ってて素晴らしい本だ。この本はプロテクトモードまでやけど、IA-32eモードの64bitモードまで試してみたいな。

我々の使っているPC(x86)はだいたいこんな感じで起動する。

電源オン → ブートセクタをメモリの0x7C00に読み込む
  → 0x7C00へ制御を移す → カーネルをメモリに読み込む
  → カーネルへ制御を移す → その他もろもろ → 起動完了

ブートセクタというのはディスクの先頭セクタ(512byte)のことで、起動ディスクのここにコードを入れて置くとBIOSがそれをメモリの0x7C00に置いて実行してくれる。このコードが起動プロセスの中で最初に実行できる、自分たちで書けるコードということになる。普通はブートローダーと呼ばれるカーネールをロードするためのプログラムが入っている。

とりあえず、BIOSが勝手に読み込んでくれる部分をブートーローダ、ブートローダの処理でメモリにロードする部分をカーネルと見立ててハローワールド的なものを作ってみる。


■環境
アセンブラ: NASM 2.07
エミュレータ: qemu 0.12.2


ソース(bootsect.s)

01: ; bootsect.s
02: bits 16
03: org 0x0
04:         jmp 0x07C0:start         ; cs=0x07C0
05: start:
06:         mov ax, cs
07:         mov ds, ax               ; ds=cs=0x07C0
08:         mov si, msg
09: putc:
10:         lodsb                    ; load a byte at "si" into "al", then inc "si"
11:         cmp al, 0x00
12:         jz load_kernel
13:         mov ah, 0x0E             ; print ascii
14:         mov bl, 0x07             ; set color white
15:         int 0x10                 ; display service
16:         jmp putc
17:         
18: load_kernel:    
19:         mov ax, 0x1000
20:         mov es, ax
21:         mov bx, 0               ; dest is [es:bx]
22:         mov ah, 2               ; read from disk
23:         mov al, 1               ; disk num
24:         mov ch, 0               ; cylinder
25:         mov cl, 2               ; sector 2
26:         mov dh, 0               ; head num
27:         mov dl, 0               ; drive num
28:         int 0x13                ; disk service
29:         
30:         jmp 0x1000:0            ; jump into the kernel
31: 
32: msg:
33:         db 'loading kernel image...', 0x0A, 0x0D, 0x00
34: padding:
35:         times 510-($-$$) db 0x00
36:         dw 0xAA55

IA32アーキテクチャではメモリアクセスは強制的にセグメント機構を経由することになる。セグメント未指定でデータへアクセスすると自動的にdsが指すセグメントを指定したことになる。なので、コードセグメント(cs)とデータセグメント(ds)は同じにしておいた方がコーディングが楽になる。

9行目〜10行目辺りはBIOSのビデオサービスを利用して文字をコンソールへ出力している。

18行目〜28行目でBIOSのディスクサービスを利用して、2番目のセクタの内容を0x1000:0に読み込んでいる。0x1000:0と言うのは <セグメント>:<実効アドレス> という形のアドレス指定。リアルアドレスモードではセグメント値は単に4ビット左シフトされて実効アドレスと足し合わすことでリニアアドレスを算出する。ページングを利用していない場合はリニアアドレス=物理アドレスとなるので 0x1000:0 は物理アドレス 0x10000 のこと。

36行目は、ブートセクタの最後の2バイトは 0x55, 0xAAでなければならないと言う決まりに従ったもの。


ソース(kernel.s)

01: ; kernel.s
02: bits 16
03: org 0x0
04:         jmp 0x01000:start
05: start:
06:         mov ax, cs
07:         mov ds, ax
08:         mov si, msg
09: putc:
10:         lodsb                   ; load a byte at "si" into "al", then inc "si"
11:         cmp al, 0x00
12:         jz exit
13:         mov ah, 0x0E            ; print ascii
14:         mov bl, 0x07            ; set color white
15:         int 10h                 ; display access
16:         jmp putc
17: exit:
18:         jmp $                   ; infinite loop
19: 
20: msg:
21:         db 'I am the Kernl!', 0x0A, 0x0D, 0x00
22: padding:
23:         times 512-($-$$) db 0x00

やってることはbootsect.sとほとんど同じ。というか少ない。カーネルだよーと言う文字を出すだけ。上で紹介した本よりもずっと手抜き。


コンパイル&イメージ作成

$ nasm -f bin -o bootsect bootsect.s
$ nasm -f bin -o kernel kernel.s
$ cat  bootsect kernel > floppy.img


qemuで実行

$ qemu -fda floppy.img -boot a


おおおおおおおおおおおお!

感動やー(ToT)


お手本のまんまじゃなくて、いろいろいじくって(手抜きして)たもんやから結構無限ループとかになって、実は手間がかかってたんです(^^;