Kernel/VM Advent Calendar 33 日目 @hdk_2
Kernel/VM Advent Calendar 33 日目 @hdk_2: gcc/as でリアルモード遊び
「低レイヤーなネタを」というお誘いがありましたので、低レイヤーなネタでアセンブラーと仲良くしていきたいと思います。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.
こんなコメントがあり、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 ビットが保存されないしで、実用性はほとんどないようです。
# 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.