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.