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 できあがったプログラムは、MS-DOS や FreeDOS の他に、MS Windows (32 ビット環境) のコマンドプロンプト (DOS 窓)、32 ビットの GNU/Linux 環境では dosemu を用いて直接実行することができます。 $ dosemu -dumb hello.com その 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, こんなコメントがあり、CFLAGS がずいぶん凝ったことになっています。 KBUILD_CFLAGS := $(LINUXINCLUDE) -g -Os -D_SETUP -D_WAKEUP -D__KERNEL__ \ code16gcc.h というのが怪しそうですね。これは arch/x86/boot/ にあります。コメントを除くとたったの 3 行です。 #ifndef __ASSEMBLY__ これが -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 あとはここに menu.lst と a.out をコピーして QEMU を実行するだけです。 $ mcopy -i fdd menu.lst ::grub えっ、GRUB2 を使っている? GRUB2 は不便で仕方がありませんね。GRUB Legacy にしましょう。 その 4: 32 ビットのリアルモードを使うリアルモードといえば 16 ビット... そう思っていた時期が私にもありました。 16 ビットではないリアルモード (?) として有名なのは big real mode でしょう。これは、リアルモードのセグメントサイズ 64KiB を、4GiB に増やしてしまうものです。プロテクトモードで設定したリミット値が、リアルモードに戻ってもそのまま生きているのでこういう芸当ができます。その 3 で紹介したプログラムでもさりげなく使っています。 で、同じやり方で、32 ビットモードで動くリアルモードが実現できます。Huge real mode と呼ばれているらしいです。GRUB の Multiboot を使えば以下のような短いプログラムで試せます: .text これはアセンブリ言語で書きましたが、32 ビットモードなので、特に小細工せずに C も使えます。しかし、BIOS は呼べないし、割り込みハンドラーを作ろうにも EIP (プログラムカウンター) の上位 16 ビットが保存されないしで、実用性はほとんどないようです。 |