x86 bootloaderでHello Worldを書いてみる

x86環境においてHello Worldを表示するbootloaderを作成し、VirtualBoxの仮想フロッピードライブから起動してみる。

環境

Ubuntu 14.04.2 LTS 64bit版、VirtualBox 4.3.28

$ uname -a
Linux vm-ubuntu64 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

アセンブリコードを書いてみる

x86環境において、BIOSから起動したbootloaderはリアルモード、すなわち16ビット命令を解釈するモードで動作する。 下記のドキュメントを参考に、BIOS割り込みルーチンを使ってHello Worldを表示するアセンブリコードを書くと次のようになる。

        /* hello.s */
        .code16
        .intel_syntax noprefix
        .globl _start
_start:
        call set_video_mode
        call set_bg_color
        lea si, msg_hello
        call print
        lea si, msg_waitkey
        call print
        call reboot

set_video_mode:
        mov ah, 0x00      /* VIDEO - SET VIDEO MODE */
        mov al, 0x12      /* 640x480x16 (VGA) */
        int 0x10
        ret

set_bg_color:
        mov ah, 0x0b      /* VIDEO - SET BACKGROUND/BORDER COLOR */
        mov bh, 0x00
        mov bl, 0x01      /* Blue */
        int 0x10
        ret

print:
        mov ah, 0x0e      /* VIDEO - TELETYPE OUTPUT */
        mov bl, 0x0f      /* White */
        mov bh, 0x00
print_loop:
        lodsb
        test al, al
        jz print_end
        int 0x10
        jmp print_loop
print_end:
        ret

reboot:
        mov ah, 0x00       /* KEYBOARD - GET KEYSTROKE */
        int 0x16
        int 0x19           /* SYSTEM - BOOTSTRAP LOADER */

msg_hello:
        .asciz "Hello, World!\r\n"
msg_waitkey:
        .asciz "\r\nPress any key to reboot..."

ここで、.code16は16ビットコードであること、.ascizはNUL文字で終端された文字列を表すGNU asディレクティブである。 上のコードは、ビデオモードを16色VGAに変更し、背景色を青にセットした後、Hello Worldを一文字ずつ出力する。 その後、キー入力を待つ文字列を出力し、キー入力を受け取ったら再起動する。

bootloaderは通常、メモリアドレス0x7c00にロードされ実行される。 そこで、コードの起点に0x7c00を指定してコンパイルし、i8086アーキテクチャ(16ビット)でのディスアセンブル結果を表示すると次のようになる。

$ gcc -nostdlib -Ttext=0x7c00 hello.s

$ objdump -d -m i8086 a.out

a.out:     file format elf64-x86-64


Disassembly of section .text:

0000000000007c00 <_start>:
    7c00:       e8 14 00                call   7c17 <set_video_mode>
    7c03:       e8 18 00                call   7c1e <set_bg_color>
    7c06:       8d 36 3d 7c             lea    si,ds:0x7c3d
    7c0a:       e8 1a 00                call   7c27 <print>
    7c0d:       8d 36 4d 7c             lea    si,ds:0x7c4d
    7c11:       e8 13 00                call   7c27 <print>
    7c14:       e8 20 00                call   7c37 <reboot>

0000000000007c17 <set_video_mode>:
    7c17:       b4 00                   mov    ah,0x0
    7c19:       b0 12                   mov    al,0x12
    7c1b:       cd 10                   int    0x10
    7c1d:       c3                      ret

0000000000007c1e <set_bg_color>:
    7c1e:       b4 0b                   mov    ah,0xb
    7c20:       b7 00                   mov    bh,0x0
    7c22:       b3 01                   mov    bl,0x1
    7c24:       cd 10                   int    0x10
    7c26:       c3                      ret

0000000000007c27 <print>:
    7c27:       b4 0e                   mov    ah,0xe
    7c29:       b3 0f                   mov    bl,0xf
    7c2b:       b7 00                   mov    bh,0x0

0000000000007c2d <print_loop>:
    7c2d:       ac                      lods   al,BYTE PTR ds:[si]
    7c2e:       84 c0                   test   al,al
    7c30:       74 04                   je     7c36 <print_end>
    7c32:       cd 10                   int    0x10
    7c34:       eb f7                   jmp    7c2d <print_loop>

0000000000007c36 <print_end>:
    7c36:       c3                      ret

0000000000007c37 <reboot>:
    7c37:       b4 00                   mov    ah,0x0
    7c39:       cd 16                   int    0x16
    7c3b:       cd 19                   int    0x19

0000000000007c3d <msg_hello>:
    7c3d:       48                      dec    ax
    7c3e:       65                      gs
    7c3f:       6c                      ins    BYTE PTR es:[di],dx
    7c40:       6c                      ins    BYTE PTR es:[di],dx
    7c41:       6f                      outs   dx,WORD PTR ds:[si]
    7c42:       2c 20                   sub    al,0x20
    7c44:       57                      push   di
    7c45:       6f                      outs   dx,WORD PTR ds:[si]
    7c46:       72 6c                   jb     7cb4 <msg_waitkey+0x67>
    7c48:       64 21 0d                and    WORD PTR fs:[di],cx
    7c4b:       0a 00                   or     al,BYTE PTR [bx+si]

0000000000007c4d <msg_waitkey>:
    7c4d:       0d 0a 50                or     ax,0x500a
    7c50:       72 65                   jb     7cb7 <msg_waitkey+0x6a>
    7c52:       73 73                   jae    7cc7 <msg_waitkey+0x7a>
    7c54:       20 61 6e                and    BYTE PTR [bx+di+0x6e],ah
    7c57:       79 20                   jns    7c79 <msg_waitkey+0x2c>
    7c59:       6b 65 79 20             imul   sp,WORD PTR [di+0x79],0x20
    7c5d:       74 6f                   je     7cce <msg_waitkey+0x81>
    7c5f:       20 72 65                and    BYTE PTR [bp+si+0x65],dh
    7c62:       62 6f 6f                bound  bp,DWORD PTR [bx+0x6f]
    7c65:       74 2e                   je     7c95 <msg_waitkey+0x48>
    7c67:       2e                      cs
    7c68:       2e                      cs
        ...

ディスアセンブル結果から、call命令やjmp命令が指すアドレスが16ビットになっていることが確認できる。

コンパイルされた機械語をC形式の文字列に変換してみる。

$ objdump -s -j.text a.out

a.out:     file format elf64-x86-64

Contents of section .text:
 7c00 e81400e8 18008d36 3d7ce81a 008d364d  .......6=|....6M
 7c10 7ce81300 e82000b4 00b012cd 10c3b40b  |.... ..........
 7c20 b700b301 cd10c3b4 0eb30fb7 00ac84c0  ................
 7c30 7404cd10 ebf7c3b4 00cd16cd 1948656c  t............Hel
 7c40 6c6f2c20 576f726c 64210d0a 000d0a50  lo, World!.....P
 7c50 72657373 20616e79 206b6579 20746f20  ress any key to
 7c60 7265626f 6f742e2e 2e00               reboot....

$ objdump -s -j.text a.out | grep "^ " | cut -d" " -f3-6 | perl -pe 's/(\w{2})\s*/\\x\1/g'
\xe8\x14\x00\xe8\x18\x00\x8d\x36\x3d\x7c\xe8\x1a\x00\x8d\x36\x4d\x7c\xe8\x13\x00\xe8\x20\x00\xb4\x00\xb0\x12\xcd\x10\xc3\xb4\x0b\xb7\x00\xb3\x01\xcd\x10\xc3\xb4\x0e\xb3\x0f\xb7\x00\xac\x84\xc0\x74\x04\xcd\x10\xeb\xf7\xc3\xb4\x00\xcd\x16\xcd\x19\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a\x00\x0d\x0a\x50\x72\x65\x73\x73\x20\x61\x6e\x79\x20\x6b\x65\x79\x20\x74\x6f\x20\x72\x65\x62\x6f\x6f\x74\x2e\x2e\x2e\x00

ディスクイメージを作ってみる

次に、上のプログラムを含むディスクイメージを作ってみる。

BIOSは各ディスクドライブの最初の1セクタ(512バイト)を読み込み、その末尾が\x55\xaaであれば起動可能ドライブと判断し、そのセクタをアドレス0x7c00にロードする。 この1セクタはMaster Boot Record(MBR)と呼ばれ、末尾のシグネチャ\x55\xaaは2進表記で01010101 10101010となるビットパターンを意味する。

Pythonスクリプトで、末尾にシグネチャを置いた512バイトのディスクイメージを作成してみる。

# create_img.py
fname = 'hello.img'

buf = '\xe8\x14\x00\xe8\x18\x00\x8d\x36\x3d\x7c\xe8\x1a\x00\x8d\x36\x4d\x7c\xe8\x13\x00\xe8\x20\x00\xb4\x00\xb0\x12\xcd\x10\xc3\xb4\x0b\xb7\x00\xb3\x01\xcd\x10\xc3\xb4\x0e\xb3\x0f\xb7\x00\xac\x84\xc0\x74\x04\xcd\x10\xeb\xf7\xc3\xb4\x00\xcd\x16\xcd\x19\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a\x00\x0d\x0a\x50\x72\x65\x73\x73\x20\x61\x6e\x79\x20\x6b\x65\x79\x20\x74\x6f\x20\x72\x65\x62\x6f\x6f\x74\x2e\x2e\x2e\x00'
buf += '\x00' * (510-len(buf))
buf += '\x55\xaa'

with open(fname, 'wb') as f:
    f.write(buf)

スクリプトを実行し、作成されたディスクイメージを16進ダンプしてみる。

$ python create_img.py

$ file hello.img
hello.img: x86 boot sector

$ hexdump -C hello.img
00000000  e8 14 00 e8 18 00 8d 36  3d 7c e8 1a 00 8d 36 4d  |.......6=|....6M|
00000010  7c e8 13 00 e8 20 00 b4  00 b0 12 cd 10 c3 b4 0b  ||.... ..........|
00000020  b7 00 b3 01 cd 10 c3 b4  0e b3 0f b7 00 ac 84 c0  |................|
00000030  74 04 cd 10 eb f7 c3 b4  00 cd 16 cd 19 48 65 6c  |t............Hel|
00000040  6c 6f 2c 20 57 6f 72 6c  64 21 0d 0a 00 0d 0a 50  |lo, World!.....P|
00000050  72 65 73 73 20 61 6e 79  20 6b 65 79 20 74 6f 20  |ress any key to |
00000060  72 65 62 6f 6f 74 2e 2e  2e 00 00 00 00 00 00 00  |reboot..........|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200

ダンプ結果から、指定した通りのディスクイメージが作成できていることがわかる。

VirtualBoxで起動してみる

VirtualBoxの仮想フロッピードライブを使い、作成したディスクイメージを読み込ませてみる。

まず、仮想マシンの新規作成を選択し、タイプ「Other」、バージョン「Other/Unknown」、「仮想ハードドライブを追加しない」を指定する。 次に、作成した仮想マシンの「設定」→「ストレージ」から「フロッピーコントローラーを追加」を選択し、作成したディスクイメージを指定する。

この仮想マシンを起動すると、次のスクリーンショットのような画面が表示される。

f:id:inaz2:20150728234424p:plain

最初のアセンブリコードで書いた通り、ブルースクリーンHello Worldが表示されている。 また、適当なキーを押すと再起動し、再びHello Worldが表示されることが確認できる。

OSが起動する仕組み

ここではHello Worldの表示を行ったが、一般的な32ビットOSが起動する際はHello Worldの代わりに次のような処理が行われる。

  • Global Descriptor Table(GDT)、Interrupt Descriptor Table(IDT)を設定する
  • A20以上のアドレスバスを有効にして、32ビットのメモリ空間を利用できるようにする
  • CPUの動作モードをリアルモード(16ビット)からプロテクトモード(32ビット)に変更する
  • セグメントセレクタ、スタックポインタを初期化する
  • ファイルシステムからカーネルイメージを検索し、メモリにロードしてジャンプする

また、bootloader全体がMBRに収まらない際、2段階に分けて実行するといったこともよく行われている。

GNU GRUB

Linux OSでは、一般にGNU GRUBと呼ばれる高機能なbootloaderが利用されている。 GNU GRUBMBRに置かれるコードは次のようになっている。

実際に、UbuntuがインストールされたディスクのMBRを書き出してみる。

$ sudo dd if=/dev/sda bs=1 count=512 > mbr.bin
512+0 records in
512+0 records out
512 bytes (512 B) copied, 0.00136663 s, 375 kB/s

$ file mbr.bin
mbr.bin: x86 boot sector

$ hexdump -C mbr.bin
00000000  eb 63 90 10 8e d0 bc 00  b0 b8 00 00 8e d8 8e c0  |.c..............|
00000010  fb be 00 7c bf 00 06 b9  00 02 f3 a4 ea 21 06 00  |...|.........!..|
00000020  00 be be 07 38 04 75 0b  83 c6 10 81 fe fe 07 75  |....8.u........u|
00000030  f3 eb 16 b4 02 b0 01 bb  00 7c b2 80 8a 74 01 8b  |.........|...t..|
00000040  4c 02 cd 13 ea 00 7c 00  00 eb fe 00 00 00 00 00  |L.....|.........|
00000050  00 00 00 00 00 00 00 00  00 00 00 80 01 00 00 00  |................|
00000060  00 00 00 00 ff fa 90 90  f6 c2 80 74 05 f6 c2 70  |...........t...p|
00000070  74 02 b2 80 ea 79 7c 00  00 31 c0 8e d8 8e d0 bc  |t....y|..1......|
00000080  00 20 fb a0 64 7c 3c ff  74 02 88 c2 52 bb 17 04  |. ..d|<.t...R...|
00000090  f6 07 03 74 06 be 88 7d  e8 17 01 be 05 7c b4 41  |...t...}.....|.A|
000000a0  bb aa 55 cd 13 5a 52 72  3d 81 fb 55 aa 75 37 83  |..U..ZRr=..U.u7.|
000000b0  e1 01 74 32 31 c0 89 44  04 40 88 44 ff 89 44 02  |..t21..D.@.D..D.|
000000c0  c7 04 10 00 66 8b 1e 5c  7c 66 89 5c 08 66 8b 1e  |....f..\|f.\.f..|
000000d0  60 7c 66 89 5c 0c c7 44  06 00 70 b4 42 cd 13 72  |`|f.\..D..p.B..r|
000000e0  05 bb 00 70 eb 76 b4 08  cd 13 73 0d 5a 84 d2 0f  |...p.v....s.Z...|
000000f0  83 d0 00 be 93 7d e9 82  00 66 0f b6 c6 88 64 ff  |.....}...f....d.|
00000100  40 66 89 44 04 0f b6 d1  c1 e2 02 88 e8 88 f4 40  |@f.D...........@|
00000110  89 44 08 0f b6 c2 c0 e8  02 66 89 04 66 a1 60 7c  |.D.......f..f.`||
00000120  66 09 c0 75 4e 66 a1 5c  7c 66 31 d2 66 f7 34 88  |f..uNf.\|f1.f.4.|
00000130  d1 31 d2 66 f7 74 04 3b  44 08 7d 37 fe c1 88 c5  |.1.f.t.;D.}7....|
00000140  30 c0 c1 e8 02 08 c1 88  d0 5a 88 c6 bb 00 70 8e  |0........Z....p.|
00000150  c3 31 db b8 01 02 cd 13  72 1e 8c c3 60 1e b9 00  |.1......r...`...|
00000160  01 8e db 31 f6 bf 00 80  8e c6 fc f3 a5 1f 61 ff  |...1..........a.|
00000170  26 5a 7c be 8e 7d eb 03  be 9d 7d e8 34 00 be a2  |&Z|..}....}.4...|
00000180  7d e8 2e 00 cd 18 eb fe  47 52 55 42 20 00 47 65  |}.......GRUB .Ge|
00000190  6f 6d 00 48 61 72 64 20  44 69 73 6b 00 52 65 61  |om.Hard Disk.Rea|
000001a0  64 00 20 45 72 72 6f 72  0d 0a 00 bb 01 00 b4 0e  |d. Error........|
000001b0  cd 10 ac 3c 00 75 f4 c3  c2 70 0e 00 00 00 80 20  |...<.u...p..... |
000001c0  21 00 83 1a 3b 1f 00 08  00 00 00 98 07 00 00 3b  |!...;..........;|
000001d0  1b 1f 05 fe ff ff fe a7  07 00 02 50 f8 04 00 00  |...........P....|
000001e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 55 aa  |..............U.|
00000200

ダンプ結果から、作成したbootloader同様、末尾にシグネチャ\x55\xaaが置かれていることが確認できる。

関連リンク