x86 bootloaderから簡単なOSカーネルを動かしてみる

「x86 bootloaderでHello Worldを書いてみる」では、リアルモード(16ビット)で動作する簡単なbootloaderを書いてみた。 ここでは、CPUの動作モードをプロテクテッドモード(32ビット)に切り替え、C言語コードからコンパイルした簡単なOSカーネルを動作させるbootloaderを書いてみる。

環境

Ubuntu 14.04.2 LTS 64bit版、VirtualBox 4.3.28

$ uname -a
Linux vm-ubuntu64 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

bootloaderを書いてみる

CPUの動作モードをプロテクテッドモードに切り替え、カーネルイメージを実行するbootloaderを書いてみると次のようになる。

/* bootloader.s */
.intel_syntax noprefix
.globl _start

.code16

_start:
        mov ax, 0x00      /* initialize stack */
        mov ss, ax
        mov sp, 0x6000

        lea si, msg_booting
        call print

        call load_kimage
        call setup_gdt
        call enable_a20_gate
        call set_video_mode
        call enter_pmode

msg_booting:
        .asciz "Booting...\r\n"

print:
        mov ah, 0x0e      /* VIDEO - TELETYPE OUTPUT */
        mov bl, 0x07      /* Light Gray */
        mov bh, 0x00
print_loop:
        lodsb
        test al, al
        jz print_end
        int 0x10
        jmp print_loop
print_end:
        ret

load_kimage:
        mov ah, 0x02      /* DISK - READ SECTOR(S) INTO MEMORY */
        mov al, 0x10      /* number of sectors to read (must be nonzero) */
        mov ch, 0x00      /* low eight bits of cylinder number */
        mov cl, 0x02      /* sector number 1-63 (bits 0-5) */
        mov dh, 0x00      /* head number */
        mov dl, 0x00      /* drive number (bit 7 set for hard disk) */
        mov bx, 0x0000    /* ES:BX -> data buffer */
        mov es, bx
        mov bx, 0x8000
        int 0x13
        jc load_failure
        mov bx, 0x6000
        mov [bx], al      /* number of sectors transferred */
        ret
load_failure:
        cli
        hlt

setup_gdt:
        cli
        pusha
        lgdt [gdt_toc]
        sti
        popa
        ret

gdt_toc:
        .word 8*3
        .word gdt, 0x0000
gdt:
        .word 0x0000, 0x0000, 0x0000, 0x0000    /* null descriptor */
        .word 0xffff, 0x0000, 0x9a00, 0x00cf    /* code descriptor */
        .word 0xffff, 0x0000, 0x9200, 0x00cf    /* data descriptor */

enable_a20_gate:
        mov ax, 0x2401    /* SYSTEM - later PS/2s - ENABLE A20 GATE */
        int 0x15
        ret

set_video_mode:
        mov ah, 0x00      /* VIDEO - SET VIDEO MODE */
        mov al, 0x13      /* VGA mode 13 (320x200x256) */
        int 0x10
        ret

enter_pmode:
        mov eax, cr0
        or eax, 1
        mov cr0, eax
        jmp 0x08:start_pmode

.code32

start_pmode:
        mov ax, 0x10           /* initialize segment selector and stack */
        mov ds, ax
        mov es, ax
        mov fs, ax
        mov gs, ax
        mov ss, ax
        mov esp, 0xfffffff0

reloc_kernel:
        cld
        mov ebx, 0x6000
        xor ecx, ecx
        mov cl, [ebx]
        shl ecx, 9             /* sector -> bytes */
        mov esi, 0x8000
        mov edi, 0x100000
        rep movsb

boot_kernel:
        cli
        mov ebp, 0x100000
        add ebp, [ebp+0x18]    /* Elf32_Ehdr e_entry */
        call ebp
        hlt

上のコードの内容を説明すると、次のようになる。

  1. boot messageを表示する
  2. カーネルイメージを読み込む
    • ここでは、ディスクの2セクタ目から10セクタ分(512バイト×10)をアドレス0x8000にロードしている
  3. GDTを設定する
    • ここでは、コードセグメント、データセグメントそれぞれがメモリ全体(0x00000000から0xFFFFF000)を指すようにしている
  4. A20 gateを有効にする
    • アドレスバスの制限を解除し、0x100000以上のアドレスを使えるようにする
    • キーボードコントローラを使う方法、Fast A20 Gateを使う方法などもあるが、ここではBIOS命令を使っている
  5. ビデオモードを変更する
    • ここでは、320x200の256色モード(VGA mode 13)に変更している
  6. CPUの動作モードをプロテクテッドモードに切り替える
    • GDTを使った4GBまでのメモリアクセス、32ビット命令を使えるようにする
    • コントロールレジスタCR0の下位1ビットを1に変え、セグメントレジスタcsの値をGDTのコードセグメントを指すように変えてジャンプする
    • cs以外のセグメントレジスタの値をGDTのデータセグメントを指すように変える
  7. カーネルイメージを再配置する
    • 0x8000からの10セクタ分を0x100000に再配置する
    • 再配置しなくても動作するが、ここではLinuxのメモリマップを意識して合わせている
  8. カーネルイメージを起動する
    • ELFヘッダのエントリポイントの値を読み、そのアドレスをcallする

このコードでは、カーネルイメージがMBRに続く2セクタ目以降に置かれていることを想定している。

カーネルイメージを書いてみる

次に、簡単な画面描画を行うカーネルイメージ(VGAドライバ)を書いてみる。 VGA mode 13では、VRAMと呼ばれるメモリが0xa0000にあり、ここから320×200バイトが各ピクセルを表す。 各ピクセルの値は0から255となり、これがパレットで設定された256色それぞれに対応する。

VRAMに256色の縦縞を描くプログラムコードを書くと次のようになる。

/* kimage.c */

void _start()
{
    int width = 320;
    int height = 200;
    unsigned char *vram = (void *)0xa0000;

    int x, y;

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            *(vram+y*width+x) = x & 0xff;
        }
    }

    /* infinite loop */
    while (1) {}
}

なお、一旦320×200のchar型配列を確保し、これに値をセットした後一度にVRAMにmemcpyする方法も可能である。 この場合のchar型配列はフレームバッファと呼ばれる。

ディスクイメージを作ってみる

bootloaderとカーネルイメージを組み合わせて、ディスクイメージを作ってみる。

まず、bootloaderを0x7c00をコードの起点としてコンパイルし、C形式の文字列に変換する。 さらに、カーネルコードをPIEでコンパイルする。 ここでは、カーネルイメージを少しでも小さくするために最適化オプション(-O1)もつけている。

$ gcc -nostdlib -Ttext=0x7c00 bootloader.s -o bootloader

$ objdump -s -j.text bootloader | grep "^ " | cut -d" " -f3-6 | perl -pe 's/(\w{2})\s*/\\x\1/g'
\xb8\x00\x00\x8e\xd0\xbc\x00\x60\x8d\x36\x1e\x7c\xe8\x1c\x00\xe8\x29\x00\xe8\x46\x00\xe8\x6b\x00\xe8\x6e\x00\xe8\x72\x00\x42\x6f\x6f\x74\x69\x6e\x67\x2e\x2e\x2e\x0d\x0a\x00\xb4\x0e\xb3\x07\xb7\x00\xac\x84\xc0\x74\x04\xcd\x10\xeb\xf7\xc3\xb4\x02\xb0\x10\xb5\x00\xb1\x02\xb6\x00\xb2\x00\xbb\x00\x00\x8e\xc3\xbb\x00\x80\xcd\x13\x72\x06\xbb\x00\x60\x88\x07\xc3\xfa\xf4\xfa\x60\x0f\x01\x16\x65\x7c\xfb\x61\xc3\x18\x00\x6b\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x9a\xcf\x00\xff\xff\x00\x00\x00\x92\xcf\x00\xb8\x01\x24\xcd\x15\xc3\xb4\x00\xb0\x13\xcd\x10\xc3\x0f\x20\xc0\x66\x83\xc8\x01\x0f\x22\xc0\xea\x9f\x7c\x08\x00\x66\xb8\x10\x00\x8e\xd8\x8e\xc0\x8e\xe0\x8e\xe8\x8e\xd0\xbc\xf0\xff\xff\xff\xfc\xbb\x00\x60\x00\x00\x31\xc9\x8a\x0b\xc1\xe1\x09\xbe\x00\x80\x00\x00\xbf\x00\x00\x10\x00\xf3\xa4\xfa\xbd\x00\x00\x10\x00\x03\x6d\x18\xff\xd5\xf4

$ gcc -m32 -nostdlib -fPIE -pie -O1 kimage.c -o kimage

1セクタ目(MBR)にbootloader、2セクタ目以降にカーネルイメージが配置されるように、ディスクイメージを作成するPythonスクリプトを書くと次のようになる。

# create_img.py
fname = 'boot.img'
kname = 'kimage'

with open(kname) as f:
    kimage = f.read()

buf = '\xb8\x00\x00\x8e\xd0\xbc\x00\x60\x8d\x36\x1e\x7c\xe8\x1c\x00\xe8\x29\x00\xe8\x46\x00\xe8\x6b\x00\xe8\x6e\x00\xe8\x72\x00\x42\x6f\x6f\x74\x69\x6e\x67\x2e\x2e\x2e\x0d\x0a\x00\xb4\x0e\xb3\x07\xb7\x00\xac\x84\xc0\x74\x04\xcd\x10\xeb\xf7\xc3\xb4\x02\xb0\x10\xb5\x00\xb1\x02\xb6\x00\xb2\x00\xbb\x00\x00\x8e\xc3\xbb\x00\x80\xcd\x13\x72\x06\xbb\x00\x60\x88\x07\xc3\xfa\xf4\xfa\x60\x0f\x01\x16\x65\x7c\xfb\x61\xc3\x18\x00\x6b\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x9a\xcf\x00\xff\xff\x00\x00\x00\x92\xcf\x00\xb8\x01\x24\xcd\x15\xc3\xb4\x00\xb0\x13\xcd\x10\xc3\x0f\x20\xc0\x66\x83\xc8\x01\x0f\x22\xc0\xea\x9f\x7c\x08\x00\x66\xb8\x10\x00\x8e\xd8\x8e\xc0\x8e\xe0\x8e\xe8\x8e\xd0\xbc\xf0\xff\xff\xff\xfc\xbb\x00\x60\x00\x00\x31\xc9\x8a\x0b\xc1\xe1\x09\xbe\x00\x80\x00\x00\xbf\x00\x00\x10\x00\xf3\xa4\xfa\xbd\x00\x00\x10\x00\x03\x6d\x18\xff\xd5\xf4'
buf += '\x00' * (510-len(buf))
buf += '\x55\xaa'

buf += kimage
buf += '\x00' * (512-(len(buf)%512))

with open(fname, 'wb') as f:
    f.write(buf)

上のコードは、kimageというファイル名のカーネルイメージを読み込み、boot.imgというファイル名のディスクイメージを作成する。

スクリプトを実行し、ディスクイメージを作成する。

$ python create_img.py

$ hd boot.img
00000000  b8 00 00 8e d0 bc 00 60  8d 36 1e 7c e8 1c 00 e8  |.......`.6.|....|
00000010  29 00 e8 46 00 e8 6b 00  e8 6e 00 e8 72 00 42 6f  |)..F..k..n..r.Bo|
00000020  6f 74 69 6e 67 2e 2e 2e  0d 0a 00 b4 0e b3 07 78  |oting..........x|
00000030  62 37 00 ac 84 c0 74 04  cd 10 eb f7 c3 b4 02 b0  |b7....t.........|
00000040  10 b5 00 b1 02 b6 00 b2  00 bb 00 00 8e c3 bb 00  |................|
00000050  80 cd 13 72 06 bb 00 60  88 07 c3 fa f4 fa 60 0f  |...r...`......`.|
00000060  01 16 78 36 35 7c fb 61  c3 18 00 6b 7c 00 00 00  |..x65|.a...k|...|
00000070  00 00 00 00 00 00 00 ff  ff 00 00 00 9a cf 00 ff  |................|
00000080  ff 00 00 00 92 cf 00 b8  01 24 cd 15 c3 b4 00 b0  |.........$......|
00000090  13 cd 10 c3 0f 78 32 30  c0 66 83 c8 01 0f 22 c0  |.....x20.f....".|
000000a0  ea 9f 7c 08 00 66 b8 10  00 8e d8 8e c0 8e e0 8e  |..|..f..........|
000000b0  e8 8e d0 bc f0 ff ff ff  fc bb 00 60 00 00 31 c9  |...........`..1.|
000000c0  8a 0b c1 e1 09 be 00 80  78 30 30 00 bf 00 00 10  |........x00.....|
000000d0  00 f3 a4 fa bd 00 00 10  00 03 6d 18 ff d5 f4 00  |..........m.....|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000210  03 00 03 00 01 00 00 00  11 02 00 00 34 00 00 00  |............4...|
(snip)

VirtualBoxで起動してみる

「x86 bootloaderでHello Worldを書いてみる」と同様の手順で仮想フロッピーコントローラにディスクイメージを指定し起動すると、次のスクリーンショットのようになる。

f:id:inaz2:20150816224616p:plain

bootloaderからカーネルイメージが実行され、256色の縦縞が表示されていることが確認できる。

一般的なOSでは、ここからさらに

  • 各種デバイスの制御
  • 割り込みに対するハンドリング
  • 物理メモリの管理
  • ページングによる仮想メモリの割り当て
  • ファイルシステムの読み書き
  • プロセスのスケジューリング・同期
  • プロセス間通信

などが実装されている。

関連リンク