Kernel/VM Advent Calendar 33 日目 @hdk_2: gcc/as でリアルモード遊び

カーネル/VM Advent Calendar : ATND 2011-01-08

「低レイヤーなネタを」というお誘いがありましたので、低レイヤーなネタでアセンブラーと仲良くしていきたいと思います。x86 のリアルモードに関するネタです。現在も広く使われている gcc と、binutils に含まれる as を使います。

その 1: リアルモードで実行されるコードをアセンブリ言語で書く

アセンブリ言語で書く、という話で何の変哲もありませんが、昔懐かしい MASM や TASM に慣れ親しんだ人には as の AT&T 表記が厄介かも知れません。ちなみに私は MASM よりも R86 (LSI C-86 試食版に付属のアセンブラー) を好んで使っていました。ジャンプ命令を自動的に SHORT か NEAR か選択してくれたり、SHORT で届かない条件ジャンプを自動的に分解してくれたり、メモリーの参照が書きやすい (BYTE PTR とか OFFSET とかの長ったらしいキーワードがいらない) ところが良いんですよね。

それはともかく、as を使った例として、DOS で動く .COM 形式の hello world を GNU/Linux 環境で作成してみると以下のようになります。

$ cat hello.s 
.code16
_start: .global _start
mov $hello, %dx
mov $9, %ah
int $0x21
ret
hello: .ascii "Hello, world\r\n$"
$ gcc -nostdlib -Wl,-Ttext,0x100,--oformat,binary \
> -o hello.com hello.s

できあがったプログラムは、MS-DOS や FreeDOS の他に、MS Windows (32 ビット環境) のコマンドプロンプト (DOS 窓)、32 ビットの GNU/Linux 環境では dosemu を用いて直接実行することができます。

$ dosemu -dumb hello.com
Hello, world

その 2: リアルモードで実行されるコードを C で書く

i386 用の gcc は 32 ビットのアセンブリコードを出力します。普通のリアルモードは 16 ビットのコードを実行するため、i386 用の gcc が吐いたコードをそのまま使うことができません。しかし、Linux のソースコードを見ると、arch/x86/kernel/acpi/realmode/ というところに、リアルモードで実行される C のコードがあります。これはいったいどういうことなのでしょうか。

というわけで、中を見てみましょう。答えは Makefile にあります。

# How to compile the 16-bit code.  Note we always compile for -march=i386,
# that way we can complain to the user if the CPU is insufficient.
# Compile with _SETUP since this is similar to the boot-time setup code.
# How to compile the 16-bit code.  Note we always compile for -march=i386,
# that way we can complain to the user if the CPU is insufficient.
# Compile with _SETUP since this is similar to the boot-time setup code.

こんなコメントがあり、CFLAGS がずいぶん凝ったことになっています。

KBUILD_CFLAGS   := $(LINUXINCLUDE) -g -Os -D_SETUP -D_WAKEUP -D__KERNEL__ \
                   -I$(srctree)/$(bootsrc) \
                   $(cflags-y) \
                   -Wall -Wstrict-prototypes \
                   -march=i386 -mregparm=3 \
                   -include $(srctree)/$(bootsrc)/code16gcc.h \
                   -fno-strict-aliasing -fomit-frame-pointer \
                   $(call cc-option, -ffreestanding) \
                   $(call cc-option, -fno-toplevel-reorder,\
                        $(call cc-option, -fno-unit-at-a-time)) \
                   $(call cc-option, -fno-stack-protector) \
                   $(call cc-option, -mpreferred-stack-boundary=2)

code16gcc.h というのが怪しそうですね。これは arch/x86/boot/ にあります。コメントを除くとたったの 3 行です。

#ifndef __ASSEMBLY__
asm(".code16gcc");
#endif

これが -include の機能で真っ先に読み込まれることになり、gcc が出力するアセンブリコードの先頭に .code16gcc というのが書かれることになります。

.code16gcc が何なのかというと、.code16 と似ていますが、単に pushf とか ret とか書いた時に、pushfw, retw ではなく、pushfl, retl としてアセンブルしてくれるというものです。これによって、必要なプリフィックスが付き、gcc の吐くコードが 16 ビットモードで実行できるようになります。もちろん、プリフィックスだらけになるので、サイズは大きくなるしパフォーマンスも期待はできません。短いけどアセンブリで書きたくないコードに向いていると言えます。

その 3: プロテクトモードで実行されるにも関わらずリアルモードの BIOS が呼べるようにする

上で書いたのは短いコード向けのものでしたが、実際は、リアルモードを使うのって、単に BIOS を呼び出したいだけという場面が多いのではないでしょうか。

というわけで、プロテクトモードからリアルモードの BIOS が呼べるといいねというプログラミング方法の紹介です。アセンブリ言語でちょっとした「中継機能」を実装するだけで、プロテクトモードから INT 命令による BIOS 呼び出しが可能になります。他は普通に 32 ビットなので C で書けば良いわけです。

こういう遊びに適しているのが GRUB の Multiboot というしくみです。ちょろっと書いただけで 32 ビットのカーネルを書き始められます。

ソースは長くなってしまったので、以下のところに置きました: benkyokai-kernelvm-33

hg clone http://www.e-hdk.com/hg/hgwebdir.cgi/benkyokai-kernelvm-33/ で複製を作ることができます。

プロテクトモードで動きますが、割り込みが発生したらリアルモードに切り替えて割り込みハンドラーを呼び出すというものです。タイマーやキーボードなどのハードウェア割り込みだけでなく、BIOS 呼び出しに使われるソフトウェア割り込みについても、まったく同じ方法でリアルモードの割り込みハンドラーを呼び出しています。

注意が必要なのはセグメントアドレスで、コードが 0x100000 以降にあるため (GNU GRUB の制約によりこれより下位のアドレスに置くことができません)、コードセグメントとスタックセグメントは、リアルモードではセグメントアドレス 0xFFFF のハイメモリーとして参照させています。しかし、ディスク BIOS の DMA 転送などがハイメモリーに対して正常動作する保証はないと思われるので、データセグメントは素直に 0 ベースとしています。

プログラムを試す時は、GNU GRUB Legacy の場合は以下のようにして読み込みます:

kernel /path/to/a.out

GNU GRUB2 の場合は以下のようになります:

multiboot /path/to/a.out

GNU GRUB Legacy は、以下のようにして、フロッピーディスクイメージなどに極めて簡単にインストールできます。QEMU などの仮想マシンで試す場合はこれがとても便利です。

$ dd if=/dev/zero of=fdd count=2880
2880+0 records in
2880+0 records out
1474560 bytes (1.5 MB) copied, 0.0171814 s, 85.8 MB/s
$ /sbin/mkfs.msdos ./fdd
mkfs.msdos 3.0.9 (31 Jan 2010)
$ mmd -i fdd grub
$ mcopy -i fdd /usr/lib/grub/i386-pc/stage1 ::grub
$ mcopy -i fdd /usr/lib/grub/i386-pc/fat_stage1_5 ::grub
$ mcopy -i fdd /usr/lib/grub/i386-pc/stage2 ::grub
$ /usr/sbin/grub
grub> device (fd0) fdd
grub> root (fd0)
 Filesystem type is fat, using whole disk
grub> setup (fd0)
 Checking if "/boot/grub/stage1" exists... no
 Checking if "/grub/stage1" exists... yes
 Checking if "/grub/stage2" exists... yes
 Checking if "/grub/fat_stage1_5" exists... yes
 Running "embed /grub/fat_stage1_5 (fd0)"... failed (this is not fatal)
 Running "embed /grub/fat_stage1_5 (fd0)"... failed (this is not fatal)
 Running "install /grub/stage1 (fd0) /grub/stage2 p /grub/menu.lst "... succeed
ed
Done.
grub> quit

あとはここに menu.lst と a.out をコピーして QEMU を実行するだけです。

$ mcopy -i fdd menu.lst ::grub
$ mcopy -i fdd a.out ::
$ qemu -fda fdd

えっ、GRUB2 を使っている? GRUB2 は不便で仕方がありませんね。GRUB Legacy にしましょう。

その 4: 32 ビットのリアルモードを使う

リアルモードといえば 16 ビット... そう思っていた時期が私にもありました。

16 ビットではないリアルモード (?) として有名なのは big real mode でしょう。これは、リアルモードのセグメントサイズ 64KiB を、4GiB に増やしてしまうものです。プロテクトモードで設定したリミット値が、リアルモードに戻ってもそのまま生きているのでこういう芸当ができます。その 3 で紹介したプログラムでもさりげなく使っています。

で、同じやり方で、32 ビットモードで動くリアルモードが実現できます。Huge real mode と呼ばれているらしいです。GRUB の Multiboot を使えば以下のような短いプログラムで試せます:

        .text
        .long   0x1BADB002, 0, -0x1BADB002
        .global _start
_start: mov     %cr0, %eax
        dec     %eax
        mov     %eax, %cr0
        ljmp    $0, $1f
1:      xor     %eax, %eax
        mov     %eax, %ds
        mov     %eax, %es
        mov     %eax, %ss
        mov     $0x10000, %esp
        mov     $msg, %esi
        mov     $0xb8000, %edi
        cld
        mov     $7, %al
1:      movsb
        stosb
        cmp     (%esi),%al
        jne     1b
1:      hlt
        jmp     1b
msg:    .ascii  "It's huge real mode now!\7"

これはアセンブリ言語で書きましたが、32 ビットモードなので、特に小細工せずに C も使えます。しかし、BIOS は呼べないし、割り込みハンドラーを作ろうにも EIP (プログラムカウンター) の上位 16 ビットが保存されないしで、実用性はほとんどないようです。