GRUBであそぼ

GRUBからelfバイナリを読み込んだあと、ページングがなかなかONにできずはまっていたのだけど、一段落したのでまとめ。今回のエントリでは、GRUBからelfバイナリを読み込み、実行するところまでを述べます。

GRUBがelfバイナリを読み込み、起動するまで

まず、GRUB自体の説明から。GRUBMultiboot Specificationに即したバイナリを読み込み、バイナリに処理が移った時点でCPUのステートを仕様通りにするためのようなブートローダです。なんと、elfバイナリの読み込みに対応しています。

Multiboot Specification は、「OS毎にブートローダ書かれてるけど、そんなことしてたらDRY*1もへったくれもないよねー。じゃあ共通仕様決めようよ」って感じで策定されたみたいです。

Multiboot Specification のだいたい

自分で作ったバイナリを動作させるために、Multiboot Specificationの詳細を追ってみましょう。3.1 OS image formatによると、

  1. OSイメージに、マルチブートヘッダを入れるんだ!
  2. マルチブートヘッダは先頭8192バイトにないとダメ!
  3. マルチブートヘッダはできるだけ前の方においてね!

と書いてあります。

elf バイナリをGRUBで動かそうとして、Unexecutable Error になるのはマルチブートヘッダが先頭8192バイトにないことが原因であることが多いです*2id:ranha-studyさんのldちゃんとしろよ な?も、gcc/asにリンクをお任せしてしまって、マルチブートヘッダが後ろに行ってしまったがために起きたのだと考えられます。

Let's ride on Multiboot Specification!

では、Multiboot Specification に従ってお手製バイナリを起動してみましょう。以上に述べた知識があれば、何をすればよいかは検討がつくはずです。つまり、

という2つの作業を行うわけです。

書いてみよう:D

さて、概要がつかめたところで実際にソースコードを書いてみましょう。まず、マルチブートヘッダの中身を決定します。詳細はOS image formatを参照してください。本文では、大雑把な概要だけを追って行きます。まず、3.1.2を見ると、

http://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
magic :must be the hexadecimal value 0x1BADB002

と書いてあります。つまり、このマジックナンバーは絶対に設置しなきゃならんのです。

次にflagです。flagは、「bootloaderに抜けた後のステート」を指定する、16ビットの値です。ステートというとわかりづらいですが、具体的にはCPUのレジスタの値であったり、メモリ上に配置されているスタックの中身であったります。下手にセットすると、OS image の boot に失敗するぞ、とも書いてあります。ここでは、後々必要になるであろうビットである0bit目(バイナリをページ境界にロードするのに必要)をセットしておきます。

最後にchecksumです。magicとflagを加算した結果がゼロになるような32bit値を設置しろとかいてあります。
文章にするとややこしいので式にすると、

magic+flag+x=0

となるようなxがチェックサムです。それを設置してあげます。

さて、gasに落とし込んだソースコード、boot.sは以下のようになります:

MULTIBOOT_HEADER_MAGIC  = 0x1BADB002
MULTIBOOT_HEADER_FLAGS  = 0x0001
CHECKSUMM = -(MULTIBOOT_HEADER_MAGIC+MULTIBOOT_HEADER_FLAGS)

.section .entry 
.code32
.globl entry
entry:
    jmp init_cpu_main # <- Entry point of this program
    .align 4
multiboot_hader :
    .long MULTIBOOT_HEADER_MAGIC
    .long MULTIBOOT_HEADER_FLAGS
    .long CHECKSUM

.globl start_hlt
start_hlt:
    hlt
    jmp start_hlt

なお、init_cpu_mainというシンボルは、別のCのソースコードmain.cで定義しておきます。特にシンボル名に制限とかはありません。

extern void start_hlt(void);

void init_cpu_main()
{
    start_hlt();
}

アセンブルとリンク

では、あなたの書いたコードをコンパイル/アセンブルし、リンクしてみましょう。アセンブルにはGNUのasを使用し、リンクにはldを使用します。ところで、リンカにメモリのマッピング方法を教えるにはどうすれば良いのでしょうか?ldでは、リンカスクリプトというスクリプトファイルを読み込み、マッピングを行うことになっています。

リンカスクリプトの書き方

では、リンカスクリプトを書いてみましょう。ポイントは、entryセクションをtextセクションの前に配置している点です。これにより、entry.o内で定義されているentryセクションをmain.oの.text、.data、.bssセクションの前方に配置してます。

ENTRY(entry)
SECTIONS
{
        . = 0x100000;
        .text :{
                *(.entry);
                *(.text);
        }
        .data :{
                *(.data);
        }
        .bss :{
                *(.bss);
        }
        end = .;
}

リンカスクリプトを用いてリンクとアセンブルを行う

Makefileを示すので、読解してみてください。

CFLAGS = -fno-builtin -nostdlib -mno-red-zone -ffreestanding -nostdinc -fno-stack-protector
DISKPATH = /disk.raw

all:
        as entry.s -o entry.o
        gcc $(CFLAGS) -c main.c -o main.o
        ld -Tld.script entry.o main.o -o entry.bin 

run:
        qemu -hda $(DISKPATH) -snapshot

CFLAGSですが、デフォルトだとLinuxの標準ライブラリや、最適化を抑えるオプションです。以上のファイル

一式を同じフォルダに入れ、makeすると、バイナリが吐き出されます*3。作ったバイナリを/bootに設置して、/boot/grub/menu.lstあたりをいじってブートできるようにしてあればこれで起動します。

DISKPATHはqemu上で動作させることを想定していれてみました。make runするとこのパスで指定されたディスクイメージをブートします。以下に、ブート成功時のスクリーンショットを示しておきます。

まとめ

今回は、Multiboot Specificationにのっとった代表的なブートローダGRUBにelfバイナリを読み込ませ、動作させてみました。なお、ソースコードgithubに上げて置いたので、そちらも参照してみてください

*1:Don't Repeat Yourself...でしたっけ。←一回言ってみたかった(w

*2:私も最初やりました(w

*3:Ubuntu 8.04上で動作確認済み。build-utilsとかのパッケージが必要かも。