リアルモードでも32bitのレジスタを使うことができる

この記事は自作OS Advent Calendar 2018の21日目の記事です。

自作OSのアドカレなので、まずは作っているCPUエミュの話をしましょう。

CPUエミュを作っている話

そもそもなぜCPUエミュを作っているのかというと、 Golangで書かれたファミコンのエミュのコードを読んでめっちゃおもしろいなって思ったのが、 CPUエミュを作ってみようと思い始めたきっかけでした。

github.com

それで一番馴染みがあるアーキテクチャであるx86を対象にGolangx86のCPUエミュ「cibo」を作りはじめました。 x86/x64を対象にしたソフトウェアエミュレータは、Type1ハイパーバイザーがあるのであまり需要がない気はしますが、 はじめて作るCPUエミュは親しみのあるアーキテクチャの方がいいでしょう。 ciboという名はBLAMEの生電社の主任科学者シボさんから取っています。 「久しぶりだな頭取!」って言って霧亥を助けに来るシーン、かっこいいですよね。 github.com

ひとまずQEMUBochsで使われているBIOSである、SeaBIOSを動作させることを目標に開発しています。 今のところ、NASMでアセンブルされた簡単なバイナリを走らせることができるくらいの進捗です。早くVGAあたりをエミュレートしてカラーの画面を出したいですね

$ cibo --debug ./samples/asm/simple_call
EIP = 0x7C00, Opcode = 0xB8
EIP = 0x7C05, Opcode = 0xBB
EIP = 0x7C0A, Opcode = 0xE8
EIP = 0x7C14, Opcode = 0x89
EIP = 0x7C16, Opcode = 0xB8
EIP = 0x7C1B, Opcode = 0xC3
EIP = 0x7C0F, Opcode = 0xE9
No mapping area: 0x0
==================== registers ====================
01: EAX = 0x1011
02: ECX = 0x0
03: EDX = 0x0
04: EBX = 0xF1
05: ESP = 0x7C04
06: EBP = 0x0
07: ESI = 0x0
08: EDI = 0x0
09: EIP = 0x0
10: CS = 0x0
11: DS = 0x0
12: SS = 0x0
13: ES = 0x0
14: FS = 0x0
15: GS = 0x0
16: EFLAGS = 0x2 (00000000000000000000000000000010)
17: MM0 = 0x0
18: MM1 = 0x0
19: MM2 = 0x0
20: MM3 = 0x0
21: MM4 = 0x0
22: MM5 = 0x0
23: MM6 = 0x0
24: MM7 = 0x0

また、Unicornのようにコードからも操作できるようにしています。

package main

import (
    "github.com/tkmru/cibo/core"
)

func main() {
    beginAddress := 0x7c00
    emu := cibo.NewEmulator(32, beginAddress, 29, false, true)
    cpu := emu.CPU
    reg := &cpu.X86registers

    emu.RAM = []byte{0xb8, 0x01, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x00, 0x75, 0x05, 0xe9, 0xef, 0x83, 0xff, 0xff, 0xb8, 0x02, 0x00, 0x00, 0x00, 0x3d, 0x02, 0x00, 0x00, 0x00, 0x74, 0xef}
    /*
       mov eax, 0x1
       cmp eax, 0x2
       jnz not_equal

     equal:
       jmp 0

     not_equal:
       mov eax, 0x2
       cmp eax, 0x2
       jz equal
   */

    reg.Init()
    emu.Run()
}

テストはmulti-architecture assembler frameworkであるKeystoneGolangバインディングと標準パッケージのtestingを使って書いています。 Golangを使ってCPUエミュのテストを書くにはこれがいいと思います。実際のテストコードはこれです。

うっかり自作CPUエミュの話が長くなってしまいましたが、自作OSのアドカレの記事なのでまあよしとしましょう。 そろそろ本題に入ります。

BIOSとは

特に説明をせずBIOSについて言及していましたが、ここで説明していきます。 BIOSというのは、Basic Input/Output Systemの略でPCの電源を入れた直後の初期化や HDDから最初のプログラムローダをメモリに読み出す役割を持つプログラムです。

BIOSはリアルモードという16bitで動作するモードで動き、 周辺機器に関する処理を行ったあと、MBR(Master Boot Record)やPBR(PartiPartition Boot Record)といったブートセクタを読み出し、 32bit/64bitで動くOSに動作を移します。

このようなBIOSですが、16bitでしか動作しないので、32bit/64bitで動作するUEFIに取って代わられようとしています。 Intel CPUでは 2020年までに CSM (BIOS 互換機能) を撤廃して UEFI に完全移行する方針のようで、 近いうちにBIOSが搭載されたデバイスは見かけなくなってしまうようです。

neriring.hatenablog.jp

なぜ、そのうちなくなるであろうBIOSを自作CPUエミュで動かそうとしているのかというと、いきなりx64のエミュを実装するのは大変かなと思ったからです。ところで、seabiosを自作CPUエミュで動かそうと思うと、seabios内で使われている命令すべてを自作エミュ上で動作するようにしておく必要があります。そのため、どんな命令が使われているのか知るためにIDAでseabiosのバイナリをディスアセンブルしてみたところ、リアルモードでも32bitのレジスタは読み書きされていることに気づきました。

リアルモードで32bitのレジスタ???

下の画像を見ると32bitのレジスタを使っている命令は先頭に0x66というopcodeがついているのが分かると思います。 0x66というopcodeはOperand-size override prefixという役割を持っていて、オペランドのサイズを上書きできる接頭辞です。 つまり、リアルモードではopcode 0x66を使うことで32bitのレジスタを読み書きできるのです!!

f:id:TAKEmaru:20181220015606p:plain
seabiosのバイナリをIDAでディスアセンブルした結果の一部

これを知って、私の好きなopcodeは0x66になりました。 16bitで動作している環境で32bitのレジスタが使えるというのはとてもおもしろいですね。 わたしはこれを自分で発見してとても驚いたのですが、 Wikipediaに書かれていたので自明なことだったようです....