Contenu connexe Similaire à Javaで作る超簡易x86エミュレータ Similaire à Javaで作る超簡易x86エミュレータ (20) Javaで作る超簡易x86エミュレータ3. 第1章 はじめに
この本は、Java で超簡易 x86 エミュレータを作りながら、x86 の仕組みを学んでい
く本です。私が今作っている x86 エミュレータの初期の頃の手順を載せてあります。
x86 エミュレータを作り始めた頃の私はあまり x86 に詳しくなかったため、x86 をほ
とんど知らない人でもこの本の内容を理解できると思います。また、x86 のいくつかの
機能は動く範囲で無視しています。これは全部説明していくと、私の知らないこともでて
くるため省きました。もっと x86 に詳しく知りたい人は、この本を読んだ後に Intel の
マニュアルを読む必要があります。それで、この本の読者の対象としてはアセンブラがな
んとなくわかる、例えば MOV が代入で ADD が足し算だとかその程度で大丈夫です。ま
た、Java に関しては全く説明しないため、それなりの知識を要求します。とはいえ、継
承だとかオーバーライドがわかり、ライブラリとしては BufferedInputStream の
read がわかれば特に問題ないと思います。その BufferedInputStream も最初の方
で少し使うだけなので、ほとんど無視できると思います。あと、Intel の命令が載ってい
るマニュアル『IA-32 インテル ® アーキテクチャ ソフトウェア・デベロッパーズ・マ
ニュアル 中巻 A』(以下マニュアル A)と『IA-32 インテル ® アーキテクチャ ソフト
ウェア・デベロッパーズ・マニュアル 中巻 B』(以下マニュアル B、AB まとめてマニュ
アル)を http://www.intel.com/jp/download/index.htm からダウンロードして
おきましょう。これがないと先に進めません。
次に本書が対象とする環境です。以下のものをインストールしておいてください。私は
Windows を使っていますが Linux でも以下のものを入れれば同じようにできるはずで
す。パスは自分で通しておいてください。
・JDK 6.0
http://java.sun.com/javase/ja/6/download.html
・nasm
http://www.nasm.us/
・bochs
http://bochs.sourceforge.net/
Linux では bochs をソースコードからビルドしてください。bochs をビルドするとき
に
./configure --enable-cpu-level=6 --enable-debugger –enable-disasm
とやってから make してください。また Windows では bochsdbg というコマンドを使
いますが、Linux の場合は dbg を抜いて bochs と入力してください。
3
4. 本稿では次のような計算機環境がお手元にあることを想定しています。
CPU とりあえず Java がそこそこ快適に動く CPU
メモリ とりあえず Java がそこそこ快適に動くメモリ
OS java や nasm、qemu が動く OS
本稿で紹介する手順はすべて次のような環境で行われたものです。
CPU Intel Core i3
メモリ 4GB
OS Windows 7
また本稿で紹介するアセンブリ言語は Intel 記法であるため
MOV AX, BX
は AX に BX の値を代入するという意味になります。
4
6. nasm add.asm -l add.list
このコマンドを打った後にエラーがなければ add というファイルと add.list という
ファイルができていると思います。add には上記アセンブリ言語をアセンブルした結果
である機械語が保存され、add.list には下記のテキストが保存されています。
1 00000000 B001 MOV AL, 0x01
2 00000002 0402 ADD AL, 0x02
3
4 fin:
5 00000004 EBFE JMP fin
6
これはアセンブリ言語と機械語の対応関係が書かれたものです。左側から3番目の16進
数(B001 や 0402)が機械語で、右側にそれに対応したアセンブリ言語の命令が書かれ
ています。この左側の機械語をこれから解読し、Java で実行できるようにプログラムを
作ります。
では、まず1行目を見てみましょう。ここでは B001 と MOV AL, 0x01 が対応して
いることが分かります。私が初めてリストを見たとき、B001 は B0 と 01 に分解でき、
B0 が代入命令(MOV)で 01 が代入される値だと予想しました。このときは Intel のマ
ニュアルを読んでなかったので本当にそうなるかはわかりませんでしたが、たまたま当
たっていました。その後も 0402 も 04 が足し算命令を表し、02 が足される値だと予想
し、EBFE は EB がジャンプを表す命令で EB がジャンプ先なのだと思い、エミュレータ
を作り始めました。結果的にこれで良かったのですが、勘だけで先に進むのは危険なので、
ここから先は Intel のマニュアルを読みながら進むことにします。私のやり方は、マニュ
アル A とマニュアル B 両方開いておき、マニュアル A から先ほどの機械語の最初の1バ
イト(B0 など)を検索していきます。試しに B0 を検索していくと何箇所か引っかかりま
すが、その中に図1のページが見つかると思います。この図のオペコードの下に並んでる
16進数がオペコードを表し、命令の下にある文字列がアセンブリ言語の命令を表してい
ます。オペコードの右には/r や+rb が書かれていますが今は無視します。
6
7. 図1 Intel のマニュアルで B0 を検索したところ
このページのおかげで B0 が MOV であることがわかります(先ほどのリストですでにわ
かっていましたが)。右の+rb はレジスタの番号を足したものがオペコードになること
を表しています。B0 は B0 に AL のレジスタ番号 0 を足したものです。CL が 1、DL が
2、BL が 3 になります。なので B3 は BL に値を代入する命令になります。次に命令の
下のアセンブリ言語ですが、これは書式を表しています。B0 の隣の MOV r8, imm8
の r8 は 8bit のレジスタという意味で imm8 は 8bit の即値(基本的に符号無し)です。
説明には imm8(即値)を r8(8bit レジスタ)に転送すると書いてあります。まぁ、r8 に
imm8 を代入するという意味です。またこの命令は2バイトなので、この命令を実行し
た場合 EIP レジスタの値を+2 します。すると次の命令にすすみます。
残りの ADD や JMP も見てみましょう。ADD はまず 04 で検索します。すでに ADD
だとわかってるので ADD で検索しても構いません。すると、04 ib というオペコードが
見つかると思います。ib は後ろに1バイトの即値が来ることを表しています。MOV には
付いていませんでしたが筆者にはその理由がわかっていません。とりあえず 04 のあとに
は1バイトの値が続きます。04 は後ろの1バイトの値を AL に加算するという説明があ
ります。次は JMP 命令です。EB で検索するか JMP で検索すると、EB cb というオペ
コードが見つかると思います。cb は ib と意味に違いはありますが、1 バイトの値が続き
7
8. ます。説明には『次の命令との相対分量だけ相対 shot ジャンプする。』とあります。
JMP 命令郡が書いてある枠の下に説明がありますが、short ジャンプは後ろに続く符号
付の値-128~127 を EIP レジスタに加算するというものです。今回作ったプログラム
の場合 EB Fe となっていて、FE は符号付の値の場合-2 なので EIP の値を-2 します。そ
して、この命令の長さである 2 を足すので現在の位置に戻り、またこの JMP 命令を実行
し、無限ループに入ります。
2.2 早速エミュレータ作り
唐突ですが、ここでエミュレータを作成してみましょう。簡単なプログラムのうちにエ
ミュレータを作っておかないと、動かすまでにする作業で時間がかかってしまうため、今
のうちに簡単なエミュレータを作っていきます。まずは形だけ作っておきます。Java で
作りますが、まずは Emulator という名前のクラスを作ります。このクラスにはプログ
ラムを格納するメモリを表す配列とプログラムカウンタを表す値と更に4つのレジスタを
配列で表したものを持たせます。では、作業開始です。Emulator.java というファイル
を作り、そのファイルに以下のコードを書いておください。
//ここから
public class Emulator{
private byte[] memory; //プログラムを格納するメモリ
private int[] registers;
private int eip; //プログラムカウンタ
public static final int DEFAULT_MEMORY_SIZE = 1 * 1024 * 1024;
public Emulator(int memorySize){
memory = new byte[memorySize];
registers = new int[4];
eip = 0;
}
public static void main(String[] args){
Emulator emulator =
new Emulator(Emulator.DEFAULT_MEMORY_SIZE);
}
}
//ここまで
この Emulator クラスはコンストラクタで渡された値の容量を持つ配列を作成します。
そして、これからこの配列にプログラムを読み込みます。Emulator クラスの最初に以
下の import 文を追加して置いてください。
8
9. //ここから
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
//ここまで
そして、以下のメソッドを追加してください。
//ここから
public void read(String fileName) throws IOException{
BufferedInputStream input = null;
try{
input =
new BufferedInputStream(new FileInputStream(fileName));
input.read(memory);
input.close();
}catch(IOException e){
if(input != null){
try{
input.close();
}catch(IOException ioe){
throw ioe;
}
}
}
}
//ここまで
そして、このメソッドを使ってファイルを読み込みます main メソッドを編集しましょ
う
//ここから
public static void main(String[] args){
if(args.length == 0){
System.out.println(“引数で読み込むファイルを指定してください”);
System.exit(0);
}
try{
9
10. Emulator emulator =
new Emulator(Emulator.DEFAULT_MEMORY_SIZE);
emulator.read(args[0]);
}catch(IOException e){
System.out.println(“ファイルの読み込みに失敗しました。”);
e.printStackTrace();
}
}
//ここまで
このコードをコンパイルして、下記コマンドを打てば add を読み込んでくれます。
java Emulator add
読み込むだけではつまらないので実行することにしましょう。Emulator クラスに
execute というメソッドを追加します。execute では、まず配列 memory の eip 番
目の値を取得します。これがオペコードとなります。このオペコードによって何をするか
を決めます。B0 の場合 MOV AL, imm8 を実行します。では試しに memory の eip
番目が B0 の場合の処理を書いてみましょう。まず、Emulator クラスに定数を追加し
ます。これは、これからレジスタの配列を扱うわけですが、単に registers[0]と書くよ
り registers[Emulator.AX]と書いた方がわかりやすいと思ったからです。では以下の
コードを public static final int DEFAULT_MEMORY_SIZE の下の行に追加してお
きましょう。
//ここから
public static final int AX = 0;
public static final int CX = 1;
public static final int DX = 2;
public static final int BX = 3;
//ここまで
それでは exexute メソッドの追加です Emulator クラスに以下のコードを書きましょ
う。
//ここから
public void execute(){
//opecode の取得(Java の byte は符号付なので符号無しの整数にする)
int code = memory[eip] & 0xFF;
//オペコードを出力しておく
System.out.printf("code = %Xn", code);
10
11. if(code == 0xB0){
//B0 の後ろに続く即値(符号無しの値として読み取る)
int value = memory[eip + 1] & 0xFF;
//何の命令を実行したか表示する
System.out.printf("MOV AL, 0x%Xn", value);
//AL レジスタに値を代入
registers[Emulator.AX] = value;
//プログラムカウンタを増加
eip += 2;
}
}
//ここまで
これで main メソッドで read でファイルを読み込んだ後に emulator.execute();を
実行すると、AL に 1 が代入されプログラムカウンタ EIP の値が2になってるはずです。
しかし、これだけでは本当に AL に 1 が代入されたか確認できないため、レジスタの値を
表示するメソッドを作ります。以下のメソッドを Emulator クラスに追加しましょう。
//ここから
public void dumpRegisters(){
System.out.println();
System.out.println("Registers Value");
System.out.printf("AX = 0x%Xn", registers[Emulator.AX]);
System.out.printf("CX = 0x%Xn", registers[Emulator.CX]);
System.out.printf("DX = 0x%Xn", registers[Emulator.DX]);
System.out.printf("BX = 0x%Xn", registers[Emulator.BX]);
System.out.printf("EIP = 0x%Xn", eip);
}
//ここまで
これを execute の後に呼び出せば AL が 1 に EIP が 2 になっていることがわかると思
います。ここで試しに main メソッドの中で execute のあとにもうひとつ execute
を入れると code = 4 が表示されると思います。これが次に実行する命令のオペコード
になるので 0x04 が表す命令を実装しましょう。execute メソッドを以下のように変更
してください。変更といっても else if 以降を追加するだけですが。
//ここから
11
12. public void execute(){
int code = memory[eip] & 0xFF;
System.out.printf("code = %Xn", code);
if(code == 0xB0){
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
registers[Emulator.AX] = value;
eip += 2;
}else if(code == 0x04){
int value = memory[eip + 1] & 0xFF;
System.out.printf("ADD AL, 0x%Xn", value);
registers[Emulator.AX] += value;
eip += 2;
}
}
//ここまで
これで、足し算ができるようになりました。main メソッドで read を呼び出した後、
execute を 2 回呼び出せすようにしてからコンパイルして実行すると足し算が行われて
いるはずです。結果を見るために dumpRegister を呼び出してみると、AL が 3、EIP
が 4 になってると思います。ここで試しに add.asm を以下のように変更して nasm
add.asm とうち Emulator を実行すると AL が 7 になっていると思います。
;ここから
MOV AL, 0x03 ;AL に 3 を代入する
ADD AL, 0x04 ;AL の値に 4 を足して AL に入れる
fin:
JMP fin ;無限ループ
;ここまで
この章の最後に JMP を実装します。JMP 命令は EB なので execute で else if を追
加いましょう。Emulator クラスの execute の else if の終わりの}の後に以下のコー
ドを追加します。
else if(code == 0xEB){
int value = memory[eip + 1]; //符号付の値を取得する
12
13. System.out.printf("JMP 0x%Xn", value & 0xFF);
eip += value;
eip += 2;
}
これで main の execute の呼び出し回数を増やしてから Emulator をコンパイルして
実行すると、dumpRegister の結果に何も変化が無いことがわかります。この上阿智
で execute を何回呼び出しても、JMP 命令が何度も実行されるだけでレジスタの値に
変化はありません。これが、何もしない無限ループに入った証拠です。これは JMP Eb
は EIP を-2 した後、命令長の 2 を足すため同じ EIP の値が変わらず同じ命令を実行し続
けるためです。
13
14. 第3章 フラグレジスタ
この章ではフラグレジスタについて説明し、エミュレータで実際にフラグレジスタを
使った条件分岐をしてみます。フラグレジスタとは、『演算の結果が 0 になった』とか
『演算の結果オーバーフローした』などといった情報をまとめたレジスタです。32bit
だと EFlags という名前になっています。EFlags は各ビットがフラグの状態を表し1だ
とそのフラグが立っていることになります。このフラグは足し算や引き算などの演算の結
果によって変化します。そのため先ほど実装した足し算のあとも本来ならフラグの更新を
行う必要があります。そして、このフラグは何に使うかというと条件分岐に使います。
『演算の結果が 0 になった』場合だけ足し算を行うなどといったように使います。今回、
全てのフラグを実装するのはめんどくさいので本稿では『演算の結果が0になった』こと
を表すゼロフラグだけ実装します。では条件分岐を行うプログラムを作っていきましょう。
以下のコードを書いてください。
;ここから
MOV AL, 0x05 ;AL に 5 を代入し
CMP AL, 0x05 ;AL と 5 を比較
JZ move16 ;結果が 0 になっていればジャンプ
fin:
JMP fin
move16:
MOV AL, 0x10 ;AL に 16(0x10)を代入
JMP fin
TIMES 510 - ($ - $$) DB 0 ; bochs に読み込ませるための
DB 0x55, 0xAA ; おまじない
;ここまで
これを comp.asm と名前を付けて保存し、
nasm comp.asm -l comp.list
とコマンドを打ちましょう。そして、できあがった comp を試しに bochs で実行しま
しょう。
bochsdbg boot:floppy "floppya: 1_44=comp, status=inserted"
すると、以下のような画面がでてくると思います(Linux の場合はでないと思います)。
14
15. ここでは右側の Simulation の下の Start ボタンを押してください。その後でてくる2
つの画面のうち、文字がたくさん出てるほうのウインドウで<bochs:1>と表示された
ら lb 0x7C00 と入力し Enter を押してください。lb はブレークポイントを仕掛けるコ
マンドです。今回の場合 0x7C00 にブレークポイントを仕掛けます。なぜ 0x7C00 か
というと bochs は読み込んだプログラムを 0x7C00 に置きます。そのためプログラム
の最初に移動するためには 0x7C00 に移動する必要があります。そしてブレークポイン
トまで行くには c コマンドで行きます。なので c と入力してください。Enter を押すと
たくさん文字がでてきて<bochs:n>と表示されると思います(n は数字)。その後、命
令を1つずつ実行するコマンド s を押して Enter を押すと1つ命令を実行します。何度
か s と Enter を押していくと MOV AL, 0x10 が実行されてると思います。Jmp -2 が
でてきたら無限ループに入ってるので r を押して Enter を押すとレジスタ一覧が表示さ
れます。そこで rax または eax の右端の2つの数字が AL ですが AL が 0 になってると
思います(他の部分は初期化してないので何らかの数字が入ってます)。ここで、
comp.list を見ると新しい命令が2つあります。CMP と JZ(別名 JE)です。CMP は2
つの値を比較してフラグレジスタを更新するという命令です。実際には引き算を行いその
結果を見てフラグレジスタを更新しています。ここでは AL が 5 で、この値から 5 を引い
てるので 0 になります。そのため演算結果が 0 になったことを示すゼロフラグが 1 にな
ります。そして、演算結果の 0 ですが、これはどこにも代入をしません。そして JZ はフ
ラグレジスタのゼロフラグを見てジャンプするという命令です。前の CMP 命令でゼロフ
ラグが 1 になっているので、ここで move16 にジャンプします。そして AL に 0x10
を代入して fin にジャンプして無限ループに入るというプログラムです。それでは
comp.list やマニュアルを見ながらエミュレータを作成していきましょう。今回の CMP
のオペコードは 0x3C で後ろに 1 バイトが続きます。後ろの1バイトと比較した結果フ
ラグレジスタを更新します。今回はゼロフラグだけに注目します。そのため Emulator
クラスには zeroFlag という boolean 型の変数を追加します。ゼロフラグが 1 の場合
zeroFlag が true になり、そうでない場合 false になります。そして JZ は、オペコー
ドが 0x74 で後ろに1バイトの符号付整数が続きます。ジャンプするかどうかは
15
16. zeroFlag を見て true ならジャンプします。では Emulator クラスを編集しましょう。
まず変数 zeroFlag を追加します。
//ここから
public class Emulator{
private byte[] memory; //プログラムを格納するメモリ
private int[] registers; //レジスタ郡
private int eip; //プログラムカウンタ
private boolean zeroFlag; //ゼロフラグ
//ここまで
そして CMP 命令の追加です。Execute の if 文の最後に以下のコードを足してください
//ここから
}else if(code == 0x3C){
//比較を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("CMP AL, 0x%Xn", value);
int result = registers[Emulator.AX] - value;
zeroFlag = result == 0 ? true : false; //演算結果が 0 なら true
eip += 2;
}
//ここまで
そして dumpRegisters で zeroFlag の状態を確認できるようにしましょう。以下の
ように dumpRegisters を編集します
//ここから
public void dumpRegisters(){
//レジスタを全て出力する
System.out.println();
System.out.println("Registers Value");
System.out.printf("AX = 0x%Xn", registers[Emulator.AX]);
System.out.printf("CX = 0x%Xn", registers[Emulator.CX]);
System.out.printf("DX = 0x%Xn", registers[Emulator.DX]);
System.out.printf("BX = 0x%Xn", registers[Emulator.BX]);
System.out.printf("EIP = 0x%Xn", eip);
16
17. //ここを追加
System.out.println("ZeroFlag = " + zeroFlag);
}
//ここまで
ここまで編集してコンパイルして実行してみましょう
java Emulator comp
で実行します。すると出力の最後に ZeroFlag = true と表示されていると思います。
それを確認したら JZ も追加しましょう。先ほどの CMP 命令の後に以下のコードを続け
てください
else if(code == 0x74){
//条件ジャンプ
int value = memory[eip + 1];
System.out.printf("JZ 0x%Xn", value);
if(zeroFlag){
eip += value;
}
eip += 2;
}
これで main メソッドの中で emulator.execute()を 5 回ほど呼ぶと無限ループに入
ります。そこでレジスタの値を確認すると AL が 0x10 となっていると思います。
そろそろ execute メソッドが長くなってきたので少し整理します。
if(code == 0xB0){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
registers[Emulator.AX] = value;
eip += 2;
}
となっていた部分を
if(code == 0xB0){
movALImm8();
}
17
18. と変更し
private void movALImm8(){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
registers[Emulator.AX] = value;
eip += 2;
}
というメソッドを追加します。1つ1つ載せていくのはめんどくさいので、現時点での全
てのソースコードを載せます
//ここから
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
public class Emulator{
private byte[] memory; //プログラムを格納するメモリ
private int[] registers; //レジスタ郡
private int eip; //プログラムカウンタ
private boolean zeroFlag; //ゼロフラグ
public static final int DEFAULT_MEMORY_SIZE = 1 * 1024 * 1024;
public static final int AX = 0;
public static final int CX = 1;
public static final int DX = 2;
public static final int BX = 3;
public Emulator(int memorySize){
memory = new byte[memorySize]; //プログラムを格納する領域の確保
registers = new int[4]; //現在はレジスタ4つしか使わない
eip = 0; //プログラムカウンタ
}
public void read(String fileName) throws IOException{
BufferedInputStream input = null;
try{
input = new BufferedInputStream(new FileInputStream(fileName));
18
19. //プログラムを読み込む
input.read(memory);
input.close();
}catch(IOException e){
if(input != null){
try{
input.close();
}catch(IOException ioe){
throw ioe;
}
}
throw e;
}
}
public void execute(){
//オペコードの取得
int code = memory[eip] & 0xFF;
//オペコードを表示する
System.out.printf("code = %Xn", code);
if(code == 0xB0){
movALImm8();
}else if(code == 0x04){
addALImm8();
}else if(code == 0xEB){
jmpShort();
}else if(code == 0x3C){
cmpALImm8();
}else if(code == 0x74){
jzShort();
}
}
private void movALImm8(){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
19
20. registers[Emulator.AX] = value;
eip += 2;
}
private void addALImm8(){
//足し算を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("ADD AL, 0x%Xn", value);
int result = registers[Emulator.AX] += value;
zeroFlag = result == 0 ? true : false;
registers[Emulator.AX] += result;
eip += 2;
}
private void jmpShort(){
//ジャンプ命令を実行する
int value = memory[eip + 1];
System.out.printf("JMP 0x%Xn", value & 0xFF);
eip += value;
eip += 2;
}
private void cmpALImm8(){
//比較を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("CMP AL, 0x%Xn", value);
int result = registers[Emulator.AX] - value;
zeroFlag = result == 0 ? true : false;
eip += 2;
}
private void jzShort(){
//条件分岐
int value = memory[eip + 1];
System.out.printf("JZ 0x%Xn", value);
if(zeroFlag){
eip += value;
20
21. }
eip += 2;
}
public void dumpRegisters(){
//レジスタを全て出力する
System.out.println();
System.out.println("Registers Value");
System.out.printf("AX = 0x%Xn", registers[Emulator.AX]);
System.out.printf("CX = 0x%Xn", registers[Emulator.CX]);
System.out.printf("DX = 0x%Xn", registers[Emulator.DX]);
System.out.printf("BLX= 0x%Xn", registers[Emulator.BX]);
System.out.printf("EIP = 0x%Xn", eip);
System.out.println("ZeroFlag = " + zeroFlag);
}
public static void main(String[] args){
if(args.length == 0){
System.out.println("引数で読み込むファイルを指定してください");
System.exit(0);
}
try{
Emulator emulator =
new Emulator(Emulator.DEFAULT_MEMORY_SIZE);
emulator.read(args[0]);
for(int i = 0; i < 5; i++){
emulator.execute();
}
emulator.dumpRegisters();
}catch(IOException e){
System.out.println("ファイルの読み込みに失敗しました。");
e.printStackTrace();
}
}
}
//ここまで
21
23. 第 4 章 Hello World!
本稿最後のプログラム Hello World!を作ります。まず今まで紹介してこなかったレジ
スタがでてきます。それは SI(Source Index)レジスタです。レジスタの番号は 6 で
16bit のレジスタです。主にメモリの転送元のアドレスを格納するために使います。あ
と今まで、でてこなかった命令をいくつか使っていますが、ここまできたらそんなに難し
くは無いはずです。では早速コードを見ていきましょう。
;ここから
ORG 0x7C00
XOR AH, AH ;AH と AH の XOR を取る。結果 AH は 0 になる
MOV AL, 0x03 ;AL に 3 を代入
INT 0x10 ;画面初期化
MOV SI, MESSAGE ;SI に HelloWorld!の先頭アドレスを入れる
MOV AH, 0x0E ;AH に 0x0E を代入
mloop:
MOV AL, [SI] ;メモリの SI のアドレスにある値を AL に入れる
OR AL, AL ;AL と AL の OR を取る(AL が 0 かどうか確かめる)
JE fin ;AL が 0 なら fin に行く
INT 0x10 ;1 文字表示(AL の値を文字コードとした文字が表示される)
ADD SI, 0x01 ;SI のアドレスを1つ進める(次の文字)
JMP mloop ;mloop:までジャンプ
fin:
JMP fin
MESSAGE:
DB "Hello World!", 0x0D, 0x0A, 0x00
TIMES 510 - ($ - $$) DB 0
DB 0x55, 0xAA
;ここまで
これを HelloWorld.asm で保存して、nasm でアセンブルします。今回はあえて-l を使い
ません
nasm HelloWorld.asm
23
24. これを bochs で実行します
bochsdbg boot:floppy "floppya: 1_44=HelloWorld, status=inserted"
今回は<bochs:1>が表示された後、c と入力してください。Hello World!が表示さ
れているはずです。これが終わったら、先ほどのコードから ORG 0x7C00 を抜いて
nasm で機械語にしてください。ORG 0x7C00 と先頭に書いておくと bochs のようにプ
ログラムカウンタ 0x7C00 から始めることを想定した機械語を出力します。本稿ではプ
ログラムカウンタを 0 から始めてるためこれがあるとうまくいきません。今回は普段私が
行っている開発スタイルでこれを実装します。今回は-l でリストを見ません。まずは
Emulator クラスを編集します。最初にコンストラクタを修正します。registers の配
列のサイズを 7 にします(SI を追加するため)。
public Emulator(int memorySize){
memory = new byte[memorySize]; //プログラムを格納する領域の確保
registers = new int[7]; //現在はレジスタ5つしか使わない
eip = 0; //プログラムカウンタ
}
Execute のコマンドの分岐の最後に下記のコードを入れます。これはまだ実装してない
命令のオペコードが来たら例外をだして処理を止めるためのものです。
else{
throw new RuntimeException(
"Not Implemented 0x" + Integer.toHexString(code)
);
}
そして main メソッドを以下のように変更してくだい。execute は無限ループ内で何度
も呼ばれるようにします。例外が来たらループから抜けます。
public static void main(String[] args){
if(args.length == 0){
System.out.println("引数で読み込むファイルを指定してください");
System.exit(0);
}
Emulator emulator =
new Emulator(Emulator.DEFAULT_MEMORY_SIZE);
try{
emulator.read(args[0]);
24
26. private void xorRM8R8(){
int modrm = memory[eip + 1] & 0xFF;
System.out.printf("ModRM = 0x%Xn", modrm);
throw new RuntimeException();
}
そして、execute の code の分岐のどこかに下記のコードを入れてください
else if(code == 0x30){
xorRM8R8();
}
これでコンパイルして実行すると、今回の ModR/M は 0xE4 であることがわかります。
これを確認すると r/m8 は AH、r8 も AH であることがわかります。本来なら
ModR/M 用の処理を入れたほうがいいのですが、今回は 0xE4 の場合は AH と AH の
XOR という処理にしたいと思います。今まで AL しか使ってなかったので問題になりま
せんでしたが、AL と AH は AX というひとつのレジスタの一部です。AH を書き換えた
ときに AL に影響がないように AH だけ書き換える必要があります。そのためのメソッド
を用意しましょう。更に、取得用のメソッドも必要になりますので追加しておきます。
//16bit レジスタの下位 8bit を書き換える
private void setRegister8Low(int index, int data){
registers[index] &= 0xFFFFFF00;
registers[index] |= (data & 0xFF);
}
//16bit レジスタの上位 8bit を書き換える
private void setRegister8High(int index, int data){
registers[index] &= 0xFFFF00FF;
registers[index] |= (data & 0xFF) << 8;
}
//16bit レジスタの下位 8bit を返す
private int getRegister8Low(int index){
return (int)registers[index] & 0xFF;
}
//16bit レジスタの上位 8bit を返す
private int getRegister8High(int index){
return (int)(registers[index] >> 8) & 0xFF;
}
これらのメソッドを使えばレジスタの一部だけを取得設定できます。これを使って
xorRM8R8 を編集しましょう。
26
27. private void xorRM8R8(){
int modrm = memory[eip + 1] & 0xFF;
if(modrm == 0xE4){
int ah = getRegister8High(Emulator.AX);
int result = ah ^ ah;
setRegister8High(Emulator.AX, result);
zeroFlag = result == 0 ? true : false;
}
eip += 2;
}
また、今まで registers を直接使っていたメソッドも変更しておきます。以下の3つのメ
ソッドを修正しておきましょう。
private void movALImm8(){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
setRegister8Low(Emulator.AX, value);
eip += 2;
}
private void addALImm8(){
//足し算を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("ADD AL, 0x%Xn", value);
int result = getRegister8Low(Emulator.AX)+ value;
setRegister8Low(Emulator.AX, result);
zeroFlag = result == 0 ? true : false;
eip += 2;
}
private void cmpALImm8(){
//比較を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("CMP AL, 0x%Xn", value);
27
28. int result = getRegister8Low(Emulator.AX) - value;
zeroFlag = result == 0 ? true : false;
eip += 2;
}
これでコンパイルして実行するとある程度命令が実行された後、0xCD という命令のとこ
ろで止まっています。0xCD は INT 命令でソフトウェア割り込みを起こします。ここで、
ソフトウェア割り込みが起こると BIOS の命令が呼び出されます。何が起こるかというと、
0xCD の次のバイトの値とレジスタの値で決まります。その命令の一部が(AT)BIOS –
OS-Wiki(http://community.osdev.info/index.php?%28AT%29BIOS)に載っています。
今回はこの中から命令を選んだので、この中を探せば見つかります。まず 0xCD の後ろの
値を取ってきましょう。Emulator クラスに以下のメソッドを追加してください。
private void interrupt(){
int index = memory[eip + 1] & 0xFF;
throw new RuntimeException("0x" + Integer.toHexString(index));
}
そして execute メソッドの分岐に以下のコードを挿入してください。
else if(code == 0xCD){
interrupt();
}
これでコンパイルして実行すると、0xCD の後ろは 0x10 だとわかります。これで先ほど
のページの 0x10 の部分を見て、更にレジスタの値を確認します。AH が 0 で AL が 3 な
のでビデオモードの設定であることがわかります。グラフィックス関係を実装するのは
ちょっと大変なので、今回はこの命令を無視します。そして、これまでのように分からな
い命令が来たら調べて実装を繰り返します。オペコード BE、B4、8A が続きますのでま
とめて載せておきます。
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;
public class Emulator{
private byte[] memory; //プログラムを格納するメモリ
private int[] registers; //レジスタ郡
private int eip; //プログラムカウンタ
private boolean zeroFlag; //ゼロフラグ
/*** この変数追加 ***/
private StringBuilder text;
28
29. public static final int DEFAULT_MEMORY_SIZE = 1 * 1024 * 1024;
public static final int AX = 0;
public static final int CX = 1;
public static final int DX = 2;
public static final int BX = 3;
public static final int SI = 6;
public Emulator(int memorySize){
memory = new byte[memorySize]; //プログラムを格納する領域の確保
registers = new int[7]; //現在はレジスタ7つ目まで使う
eip = 0; //プログラムカウンタ
/*** この初期化追加 ***/
text = new StringBuilder();
}
public void read(String fileName) throws IOException{
BufferedInputStream input = null;
try{
input = new BufferedInputStream(new FileInputStream(fileName));
//プログラムを読み込む
input.read(memory);
input.close();
}catch(IOException e){
if(input != null){
try{
input.close();
}catch(IOException ioe){
throw ioe;
}
}
throw e;
}
}
public void execute(){
//オペコードの取得
int code = memory[eip] & 0xFF;
29
30. //オペコードを表示する
System.out.printf("code = %Xn", code);
if(code == 0xB0){
movALImm8();
}else if(code == 0x04){
addALImm8();
}else if(code == 0xEB){
jmpShort();
}else if(code == 0x3C){
cmpALImm8();
}else if(code == 0x74){
jzShort();
}else if(code == 0x30){
xorRM8R8();
}else if(code == 0xCD){
interrupt();
}
/*** ここから追加 ***/
else if(code == 0xBE){
movSIImm16();
}else if(code == 0xB4){
movAHImm8();
}else if(code == 0x8A){
movR8RM8();
}else if(code == 0x08){
orRM8R8();
}else if(code == 0x83){
addRM16Imm8();
}
/*** ここまで追加 ***/
else{
throw new RuntimeException("Not Implemented 0x" +
Integer.toHexString(code));
}
}
private void movALImm8(){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AL, 0x%Xn", value);
30
31. setRegister8Low(Emulator.AX, value);
eip += 2;
}
private void addALImm8(){
//足し算を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("ADD AL, 0x%Xn", value);
int result = getRegister8Low(Emulator.AX)+ value;
setRegister8Low(Emulator.AX, result);
zeroFlag = result == 0 ? true : false;
eip += 2;
}
private void jmpShort(){
//ジャンプ命令を実行する
int value = memory[eip + 1];
System.out.printf("JMP 0x%Xn", value & 0xFF);
eip += value;
eip += 2;
}
private void cmpALImm8(){
//比較を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("CMP AL, 0x%Xn", value);
int result = getRegister8Low(Emulator.AX) - value;
zeroFlag = result == 0 ? true : false;
eip += 2;
}
private void jzShort(){
//条件分岐
int value = memory[eip + 1];
System.out.printf("JZ 0x%Xn", value);
if(zeroFlag){
31
32. eip += value;
}
eip += 2;
}
private void xorRM8R8(){
int modrm = memory[eip + 1] & 0xFF;
if(modrm == 0xE4){
System.out.println("XOR AH, AH");
int ah = getRegister8High(Emulator.AX);
int result = ah ^ ah;
setRegister8High(Emulator.AX, result);
zeroFlag = result == 0 ? true : false;
}else{
throw new RuntimeException("xorRM8R8 Not Implemented modrm
= " + Integer.toHexString(modrm));
}
eip += 2;
}
private void interrupt(){
int index = memory[eip + 1] & 0xFF;
int al = getRegister8Low(Emulator.AX);
int ah = getRegister8High(Emulator.AX);
if(index == 0x10){
System.out.println("INT 0x10");
if(ah == 0 && al == 0x03){
//このときは何もしない
}else if(ah == 0x0E){
/*** ここを追加 ***/
if(al == 'n'){
System.out.println(text);
32
33. throw new RuntimeException();
}else{
System.out.println((char)al);
text.append((char)al);
}
/*** ここまで ***/
}else{
throw new RuntimeException("INT 0x10 実装されてない命令です
");
}
}else{
throw new RuntimeException("INT 0x" + Integer.toHexString(index)
+ " 実装されてない命令です");
}
eip += 2;
}
/*** ここから追加 ***/
private void movAHImm8(){
//代入を行う
int value = memory[eip + 1] & 0xFF;
System.out.printf("MOV AH, 0x%Xn", value);
setRegister8High(Emulator.AX, value);
eip += 2;
}
private void movSIImm16(){
//代入を行う
int value = (memory[eip + 1] & 0xFF) | (memory[eip + 2] & 0xFF) <<
8;
System.out.printf("MOV SI, 0x%Xn", value);
registers[Emulator.SI] = value;
eip += 3;
}
private void movR8RM8(){
//代入を行う
33
34. int modrm = memory[eip + 1] & 0xFF;
if(modrm == 0x04){
System.out.println("MOV AL, [SI]");
setRegister8Low(Emulator.AX, memory[registers[Emulator.SI]] &
0xFF);
}else{
throw new RuntimeException("movR8RM8 Not Implemented
modrm = " + Integer.toHexString(modrm));
}
eip += 2;
}
private void orRM8R8(){
int modrm = memory[eip + 1] & 0xFF;
if(modrm == 0xC0){
System.out.println("OR AL, AL");
int al = getRegister8Low(Emulator.AX);
int result = al | al;
setRegister8Low(Emulator.AX, result);
zeroFlag = result == 0 ? true : false;
}else{
throw new RuntimeException("xorRM8R8 Not Implemented modrm
= " + Integer.toHexString(modrm));
}
eip += 2;
}
private void addRM16Imm8(){
int modrm = memory[eip + 1] & 0xFF;
if(modrm == 0xC6){
int value = memory[eip + 2] & 0xFF;
int result = registers[Emulator.SI] + value;
registers[Emulator.SI] = result;
zeroFlag = result == 0 ? true : false;
34
35. }
eip += 3;
}
/*** ここまで追加 **/
//16bit レジスタの下位 8bit を書き換える
private void setRegister8Low(int index, int data){
registers[index] &= 0xFFFFFF00;
registers[index] |= (data & 0xFF);
}
//16bit レジスタの上位 8bit を書き換える
private void setRegister8High(int index, int data){
registers[index] &= 0xFFFF00FF;
registers[index] |= (data & 0xFF) << 8;
}
//16bit レジスタの下位 8bit を返す
private int getRegister8Low(int index){
return registers[index] & 0xFF;
}
//16bit レジスタの上位 8bit を返す
private int getRegister8High(int index){
return (registers[index] >> 8) & 0xFF;
}
public void dumpRegisters(){
//レジスタを全て出力する
System.out.println();
System.out.println("Registers Value");
System.out.printf("AX = 0x%Xn", registers[Emulator.AX]);
System.out.printf("CX = 0x%Xn", registers[Emulator.CX]);
System.out.printf("DX = 0x%Xn", registers[Emulator.DX]);
System.out.printf("BX = 0x%Xn", registers[Emulator.BX]);
System.out.printf("EIP = 0x%Xn", eip);
System.out.println("ZeroFlag = " + zeroFlag);
}
public static void main(String[] args){
if(args.length == 0){
35
36. System.out.println("引数で読み込むファイルを指定してください");
System.exit(0);
}
Emulator emulator = new Emulator(Emulator.DEFAULT_MEMORY_SIZE);
try{
emulator.read(args[0]);
while(true){
emulator.execute();
}
}catch(IOException e){
System.out.println("ファイルの読み込みに失敗しました。");
e.printStackTrace();
}catch(RuntimeException e){
emulator.dumpRegisters();
e.printStackTrace();
}
}
}
この Emulator プログラムは http://d.hatena.ne.jp/d-kami/20110809/1312899556
に載せておきました。ここで重要なので interrupt で追加した AH == 0x03 のときの処
理で、このとき AL の値を文字コードとみなして、その文字を出力するというものです。
ただし、今回は表示用の画面を作ってないので、標準出力を利用しています。また
movSIImm16 メソッドで 16bit の値を取得していますが、この値はリトルエンディアン
で格納されてることに注意してください。あと CMP AL, 0 の代わりに OR AL, AL を使っ
ています。これは OR もフラグレジスタを書き換えるためできることです。OR は渡され
た両方の値が0のとき結果も 0 になります。最後にこのコードをコンパイルして実行する
と Hello World!が出力されてプログラムがレジスタダンプがされてプログラムが終了す
ると思います。これは文字コードn が来たときに今まで来た文字を全部出力した後に強
制的に例外を発生させてるためで、ここで例外を発生させなければ無限ループに入ります。
以上で今回作るプログラムは終了です。
36
37. 第 5 章 終わりに
本稿では x86 の機械語を解説しながら Java で x86 エミュレータを作りました。私は
ろくに知識がないまま x86 エミュレータを作り始めたので苦労も多かったのですが、本
稿を読んで少しでも機械語に興味を持ってくれる人がいたら幸いです。
実際に x86 エミュレータを作るとなると、今回のような手抜きのものではなく、もっ
と知らないといけないことが沢山あります。それでも諦めずに少しずつ作っていけば、
Linux などを実行できるエミュレータができるかもしれません。でも筆者はまだそこま
で到達していません。本稿を読んだくれた誰かがエミュレータを作り始めたら私は嬉しい
です
連絡先
kami229@hotmail.com
ブログ
http://d.hatena.ne.jp/d-kami/
37