GRUBで簡単なOSカーネルを動かしてみる

「x86 bootloaderから簡単なOSカーネルを動かしてみる」では、bootloaderを作った上で簡単なOSカーネルを動かした。 ここでは、GRUBをbootloaderとして利用してOSカーネルを動かしてみる。

環境

Ubuntu 14.04.3 LTS 64bit版、QEMU 2.0.0

$ uname -a
Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 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.3 LTS
Release:        14.04
Codename:       trusty

$ as --version
GNU assembler (GNU Binutils for Ubuntu) 2.24

$ ld --version
GNU ld (GNU Binutils for Ubuntu) 2.24

$ grub-mkrescue --version
grub-mkrescue (GRUB) 2.02~beta2-9ubuntu1.6

$ qemu-system-x86_64 --version
QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.21), Copyright (c) 2003-2008 Fabrice Bellard

$ make --version
GNU Make 3.81

必要なパッケージをインストールする

以降の手順で必要となるパッケージを次のようにしてインストールする。

$ sudo apt-get install build-essential xorriso qemu

マルチブートヘッダーを書く

まず、GRUBマルチブート用のヘッダーを用意する。 具体的には、次のようなファイルを書く。

# multiboot_header.s

.section .multiboot_header
header_start:
    .long 0xe85250d6                 # magic number (multiboot 2)
    .long 0                          # architecture 0 (protected mode i386)
    .long header_end - header_start  # header length
    # checksum
    .long 0x100000000 - (0xe85250d6 + 0 + (header_end - header_start))

    # insert optional multiboot tags here

    # required end tag
    .word 0    # type
    .word 0    # flags
    .long 8    # size
header_end:

カーネルコードを書く

次に、実際に動かすカーネルのコードを書く。 ここでは、スクリーンにOKと表示する次のような簡単なコードを使うことにする。

# kernel.s

.intel_syntax noprefix
.global kernel_start

.section .text
.code32
kernel_start:
    # print `OK` to screen
    mov dword ptr [0xb8000], 0x2f4b2f4f
    hlt

0xb8000はVGAのtext-modeバッファが位置する物理メモリアドレスである。 上のコードでは、このアドレスに緑背景白文字のOKを書き込んでいる。

カーネルイメージを作る

作成したマルチブートヘッダとカーネルコードをリンクし、カーネルイメージを作る。 まず、次のようなリンカスクリプトを書く。

/* linker.ld */

ENTRY(kernel_start)

SECTIONS {
    . = 1M;

    .text :
    {
        /* ensure that the multiboot header is at the beginning */
        *(.multiboot_header)
        *(.text)
    }
}

このリンカスクリプトを利用して、次のようにしてカーネルイメージを作る。

$ as -o multiboot_header.o multiboot_header.s
$ as -o kernel.o kernel.s
$ ld -n -o kernel.bin -T linker.ld multiboot_header.o kernel.o

実際に作成されたイメージを調べると、マルチブートヘッダの後にカーネルコードが置かれていることが確認できる。

$ objdump -d kernel.bin

kernel.bin:     file format elf64-x86-64


Disassembly of section .text:

0000000000100000 <header_start>:
  100000:       d6                      (bad)
  100001:       50                      push   rax
  100002:       52                      push   rdx
  100003:       e8 00 00 00 00          call   100008 <header_start+0x8>
  100008:       18 00                   sbb    BYTE PTR [rax],al
  10000a:       00 00                   add    BYTE PTR [rax],al
  10000c:       12 af ad 17 00 00       adc    ch,BYTE PTR [rdi+0x17ad]
  100012:       00 00                   add    BYTE PTR [rax],al
  100014:       08 00                   or     BYTE PTR [rax],al
        ...

0000000000100018 <kernel_start>:
  100018:       c7 05 00 80 0b 00 4f    mov    DWORD PTR [rip+0xb8000],0x2f4b2f4f        # 1b8022 <kernel_start+0xb800a>
  10001f:       2f 4b 2f
  100022:       f4                      hlt

ISOファイルを作る

最後に、上で作成したカーネルイメージをもとに起動可能なISOファイルを作成する。 まず、次のようなGRUBのコンフィグファイルを用意する。

# grub.cfg

set timeout=0
set default=0

menuentry "test os" {
    multiboot2 /boot/kernel.bin
    boot
}

次に、以下のようなディレクトリ構成でファイルを配置し、grub-mkrescueコマンドでISOファイルを作成する。

$ mkdir -p isofiles/boot/grub
$ cp grub.cfg isofiles/boot/grub/
$ cp kernel.bin isofiles/boot/
$ grub-mkrescue -o os.iso isofiles/

QEMUで動かしてみる

QEMUを使い、作成したISOファイルから仮想マシンを起動するには次のようにする。

$ qemu-system-x86_64 -drive format=raw,file=os.iso

起動後のスクリーンショットを次に示す。

f:id:inaz2:20151231215856p:plain

左上にOKと表示されていることから、正常にカーネルコードが実行できていることがわかる。

Makefileを書いてみる

ここまでの手順をMakefileで書くと次のようになる。

# Makefile

kernel.bin: multiboot_header.o kernel.o
        $(LD) -n -o $@ -T linker.ld $^

os.iso: grub.cfg kernel.bin
        mkdir -p isofiles/boot/grub
        cp grub.cfg isofiles/boot/grub/
        cp kernel.bin isofiles/boot/
        grub-mkrescue -o $@ isofiles/
        $(RM) -r isofiles/

.PHONY: clean run
clean:
        $(RM) multiboot_header.o kernel.o kernel.bin os.iso

run: os.iso
        qemu-system-x86_64 -drive format=raw,file=$<

$@はターゲットのファイル名、$<は依存関係にある最初のファイル、$^は依存関係にあるファイルすべてを表す。 また、.PHONYは右に書かれた名前のターゲットがファイルと無関係にコマンドとして実行されることを表すものである。 なお、.cから.oを生成するルールは省略されており、標準で定義されているものが利用されるようになっている。

このようなMakefileを用意することで、一連の手順を次のようにまとめて実行することができる。

$ make run
as   -o multiboot_header.o multiboot_header.s
as   -o kernel.o kernel.s
ld -n -o kernel.bin -T linker.ld multiboot_header.o kernel.o
mkdir -p isofiles/boot/grub
cp grub.cfg isofiles/boot/grub/
cp kernel.bin isofiles/boot/
grub-mkrescue -o os.iso isofiles/
xorriso 1.3.2 : RockRidge filesystem manipulator, libburnia project.

Drive current: -outdev 'stdio:os.iso'
Media current: stdio file, overwriteable
Media status : is blank
Media summary: 0 sessions, 0 data blocks, 0 data, 23.7g free
Added to ISO image: directory '/'='/tmp/grub.731jtJ'
xorriso : UPDATE : 281 files added in 1 seconds
Added to ISO image: directory '/'='/home/user/tmp/grub/isofiles'
xorriso : UPDATE : 285 files added in 1 seconds
xorriso : NOTE : Copying to System Area: 512 bytes from file '/usr/lib/grub/i386-pc/boot_hybrid.img'
xorriso : UPDATE :  100.00% done
ISO image produced: 2481 sectors
Written to medium : 2481 sectors at LBA 0
Writing to 'stdio:os.iso' completed successfully.

rm -f -r isofiles/
qemu-system-x86_64 -drive format=raw,file=os.iso

上の結果から、依存するファイルが順に生成されながらコマンドが実行されていることがわかる。

なお、次のようにすれば生成されたファイル一式を削除することができる。

$ make clean
rm -f multiboot_header.o kernel.o kernel.bin os.iso

関連リンク