8086 で遊べ!

今やパソコンの CPU は 64 ビット、メモリは数 GB、HDD は数 TB の時代です。一昔前と比べたら随分な進歩です。

が、現在人気の intel Core i なんとかみたいな最新の 64 ビット CPU でも、電源投入直後は 1978 年に発表されたインテル 8086 と互換性のあるリアルモードで動作します。ここから OS を起動する手続きの中で 32 ビットのプロテクトモード64 ビットモードという風に CPU の動作モードを順番に切り替えていくようです。

プロテクトモードに移行するにはメモリ上にセグメントディスクリプタテーブルだの割り込みディスクリプタテーブルだのを作成するとか何とかややこしいようですが、リアルモードならそんなの関係ありません。

ということで、フリーの仮想マシン環境の VirtualBox でリアルモードを体験してみました。BIOS コールを使って画面に “Hello, world!” と表示するだけです。BIOS 使うので VirtualBox インスタンスの設定で EFI をオンにしないように注意しましょう。

なお、8086 はまず 0xFFFF0 番地からプログラムを実行開始します。IBM PC では 0xA0000 (640 KiB) から 0xFFFFF まではメモリマップト IO で、0xFFFF0 は BIOS の領域です。BIOS はブートデバイスの最初の 1 セクタ (MBR) を 0x07C00 にロードしてここにジャンプするらしいです。(EFI と GPT の時はどうなるんでしょう?)

この記事が面白そうです!

下が GNU Assembler 用の Hello world (hello.S) です:

# vim:ft=gas:
.code16                                 # 以下リアルモード (16 ビットコード)。
.text  
        .globl  _start;
_start:                                 # リンカのために _start シンボルが必要。

        /* 画面に文字を表示するには、ah に 0x0e を、al に ASCII コードを
         * 入れて int 0x10 を実行します。
         * bh はページ番号、bl は色、の指定にそれぞれ使うらしいです。
         */
        movb    $0x0e, %ah
        movw    $(msg_end - msg), %cx   # 文字列の長さを cx に入れます。
        movw    $(msg), %si             # 文字列の開始アドレスを si に入れます。
puts:  
        lodsb                           # %ds:(%si) から1バイト
                                        # 取り出して al に入れます。
        /* lodsb の代わりにこうしても同じです (生成される機械語は違います):
        movb    %ds:(%si), %al
        inc     %si
         */
        int     $0x10                   # BIOS 呼び出し。
        dec     %cx                     # カウンタをデクリメント。
        jnz     puts                    # カウンタがゼロでなければループ。

        hlt                             # CPU を待機状態にします。でないと、
                                        # 下の "Hello, world!" を機械語として
                                        # 実行してしまいます。
msg:   
        .string "Hello, world!"
msg_end:

/* ブートフラグ */
        . = _start + 510                # ここは開始アドレスから 510 バイト目。
        .word   0xaa55                  # これがブートフラグらしいです。

ビルドに使う Rakefile も作ってみました。バイナリを作ったあと、フロッピーイメージを作ります:

task default: "hello.img"

FLOPPY_SIZE = 1440 * 1024
BLOCK_SIZE = 512

file "hello.o" => "hello.S" do |t|
  sh "as #{t.source} -o #{t.name}"
end

file "hello.bin" => "hello.o" do |t|
  # MBR は 0x7c00 にロードされます
  sh "ld -Ttext 0x7c00 --oformat=binary #{t.source} -o #{t.name}"
end     

file "hello.img" => "hello.bin" do |t|  
  # まずゼロフィルした空のフロッピーイメージを用意します
  sh "dd if=/dev/zero \
         of=#{t.name} \
         bs=#{BLOCK_SIZE} \
         count=#{FLOPPY_SIZE/BLOCK_SIZE}"
        
  # フロッピーイメージの先頭に hello.bin を書き込みます
  File.open t.name, "r+b" do |f|        
    f.write File.binread(t.source)
  end   
end                                     
                                        
task :clean do
  rm %w(hello.o hello.bin hello.img)
end

これが VirtulBox での実行結果です:

Hello, world!

すばらしい!

ところで、上のソースコードで “Hello, world!” を機械語として実行する、と書きましたが、実際この文字列は機械語としてどんな意味があるんでしょう? objdump で調べてみました。

対象は以下のソースコードから作られたバイナリです:

.code16
.text
        .globl _start
_start:
        .string "Hello, world!"

下が objdump -m i386 -b binary -D で逆アセンブルした結果です:

string.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   48                      dec    %eax
   1:   65                      gs
   2:   6c                      insb   (%dx),%es:(%edi)
   3:   6c                      insb   (%dx),%es:(%edi)
   4:   6f                      outsl  %ds:(%esi),(%dx)
   5:   2c 20                   sub    $0x20,%al
   7:   77 6f                   ja     0x78
   9:   72 6c                   jb     0x77
   b:   64 21 00                and    %eax,%fs:(%eax)

何してるのか全く分かりません…。そしてこの後、どこかのメモリ領域に闖入するわけですね。恐ろしい。

Hello, world! がマスターできたところで、BIOS を使ってディスクの内容を読み込んだり、VGA でお絵かきしたり、ということができたらまた面白いかも知れません。何だか昔のマイコンみたい。PC って OS があって当然、みたいに思い込んでたので、新鮮な気分です。

(コウヅ)