SlideShare a Scribd company logo
Java で作る超簡易
x86 エミュレータ
d-kami
目次
第1章 はじめに
第2章 アセンブラとエミュレータ
第3章 フラグレジスタ
第4章 Hello World
第5章 終わりに
2
第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
 本稿では次のような計算機環境がお手元にあることを想定しています。
CPU とりあえず Java がそこそこ快適に動く CPU
メモリ とりあえず Java がそこそこ快適に動くメモリ
OS java や nasm、qemu が動く OS
 本稿で紹介する手順はすべて次のような環境で行われたものです。
CPU Intel Core i3
メモリ 4GB
OS Windows 7
 また本稿で紹介するアセンブリ言語は Intel 記法であるため
MOV AX, BX
は AX に BX の値を代入するという意味になります。
4
第2章 アセンブルしよう
2.1 足し算をしてみよう
 この章ではアセンブリ言語で簡単なプログラムを作り、機械語だけのファイルと命令と
機械語の組み合わせたもののリストを出力させるようにします。出力した機械語だけの
ファイルを実行し、命令と機械語の組み合わせたもののリスト(以下リスト)を読んで、機
械語とアセンブリ言語の命令の対応を学びます。では早速アセンブリ言語と行きたい所で
すが、まず学ぶものがあります。それは AX、CX、DX、BX という4つのレジスタです。
これから作るエミュレータは x86 というシリーズの CPU を対象に作り、アセンブリ言
語も x86 用に書きます。その x86 ででてくる値を記憶する場所がレジスタです。よくわ
からなければ、アセンブリ言語で使える特殊な変数だと思って進めてください。これらは
16bit のレジスタで 32bit だと EAX のように前に E がまた、64bit だた RAX のよう
に R がつきます。今回のプログラムでは AL というレジスタがでてきますが、AL は AX
の下位 8 ビットです。上位 8bit は AH というレジスタになっています。
↓の全体が AX レジスタ
AH レジスタ AL レジスタ
あと EIP レジスタというのがでてきます。これはプログラムカウンタと言って、現在実
行する命令の位置を指すレジスタです。命令を実行するたびに実行した命令の長さを足し
ていきますただし、このレジスタをアセンブリ言語で直接操作することはありませんがエ
ミュレータを作るときにでてきます。覚えておきましょう。では、アセンブリ言語を書い
てみましょう。その前に作業をするためのフォルダを作り(asm とか名前を付けて)、コ
マンドプロンプトでそのフォルダに移動しておきしょう。
;ここから
MOV AL, 0x01 ;AL に1を代入する
ADD AL, 0x02 ;AL の値に2を足して AL に入れる
fin:
JMP fin ;無限ループ
;ここまで
これだけです。内容はコメントである;から行の終わりまでを見ればわかると思います。
AL に1を入れて、2を足したものを AL に入れています。結果 AL には3が入っていま
す。これをアセンブルします。ファイル名は add.asm として上記内容を保存し、以下
のコマンドを打ちます。
5
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
図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
ます。説明には『次の命令との相対分量だけ相対 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
//ここから
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
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
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
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
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
第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
ここでは右側の 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
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
//ここを追加
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
と変更し
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
//プログラムを読み込む
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
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
}
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
一応、これをコンパイルして実行してみましょう。先ほどと同じ結果になると思います。
ならないと困りますが。変わったところとしては足し算でゼロフラグを更新するようにし
ています。これは ADD もフラグレジスタを更新するためです。それでは次の章に行きま
しょう。
22
第 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
これを 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
while(true){
emulator.execute();
}
}catch(IOException e){
System.out.println("ファイルの読み込みに失敗しました。");
e.printStackTrace();
}catch(RuntimeException e){
emulator.dumpRegisters();
e.printStackTrace();
}
}
//ここまで
これをコンパイルしたら以下のコマンドで HelloWorld を実行します。
Java Emulator HelloWorld
そうすると java.lang.RuntimeException: Not Implemented 0x30 という表
示が途中にでると思います。これはオペコード 0x30 を実装してないよ!という意味に
なります。マニュアルで 0x30 を探してみましょう。するとマニュアル B で XOR が見
つかります。r/m8 と r8 の XOR を取ると書いてあります。r/m8 と r8 ですが、これは
ModR/M バイトというオペコードの後ろに続く値があります。これは r/m と r(もしく
はオペコード)というように分解できます。実際には3分割されていて1バイトの最初の
2bit が mod 部、真ん中の 3bit が r 部、最後の 3bit が r/m 部です。もし ModR/M が
F3 だった場合、2進数で 11110011 になりますが、これは以下のように分解すること
ができます。mod 部と r/m 部はセットで r/m8 のような表記になっています。
mod r(もしくはオペコード) r/m
11 110 011
そしてマニュアル A の 35 ページにこの値と使われるレジスタ、もしくはメモリの番地の
割り当てが載っています。例えば上の例だと mod 部が 11 で r/m 部が 011 なので r/m
は EBX、BX、BL、MM3、XMM3 のどれかということになります。これは命令とアド
レスサイズによって特定されます。今回の 0x30 で上の例だと BL が選ばれます。そして
r は 011 なので r8(8 は 8bit の意味)の場合 DH が選ばれます。これが r16 となってる
場合は SI が選ばれます。なので上の例の場合 XOR だと BL と DH の XOR ということに
なります。 では今回の XOR の後ろに続く ModR/M を確認するために以下のメソッド
を Emulator クラスに追加します。
25
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
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
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
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
//オペコードを表示する
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
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
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
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
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
}
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
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
第 5 章 終わりに
 本稿では x86 の機械語を解説しながら Java で x86 エミュレータを作りました。私は
ろくに知識がないまま x86 エミュレータを作り始めたので苦労も多かったのですが、本
稿を読んで少しでも機械語に興味を持ってくれる人がいたら幸いです。
 実際に x86 エミュレータを作るとなると、今回のような手抜きのものではなく、もっ
と知らないといけないことが沢山あります。それでも諦めずに少しずつ作っていけば、
Linux などを実行できるエミュレータができるかもしれません。でも筆者はまだそこま
で到達していません。本稿を読んだくれた誰かがエミュレータを作り始めたら私は嬉しい
です
連絡先
 kami229@hotmail.com
ブログ
 http://d.hatena.ne.jp/d-kami/
37

More Related Content

PPTX
Kubernetesでの性能解析 ~なんとなく遅いからの脱却~(Kubernetes Meetup Tokyo #33 発表資料)
PDF
テスト文字列に「うんこ」と入れるな
PDF
分割と整合性と戦う
PPTX
ちゃんとした C# プログラムを書けるようになる実践的な方法~ Visual Studio を使った 高品質・低コスト・保守性の高い開発
PPTX
Linuxのsemaphoreとmutexを見る 
PDF
MarkdownをBacklogのwikiに変換するPWA
PDF
GPUが100倍速いという神話をぶち殺せたらいいな ver.2013
PDF
明日使えないすごいビット演算
Kubernetesでの性能解析 ~なんとなく遅いからの脱却~(Kubernetes Meetup Tokyo #33 発表資料)
テスト文字列に「うんこ」と入れるな
分割と整合性と戦う
ちゃんとした C# プログラムを書けるようになる実践的な方法~ Visual Studio を使った 高品質・低コスト・保守性の高い開発
Linuxのsemaphoreとmutexを見る 
MarkdownをBacklogのwikiに変換するPWA
GPUが100倍速いという神話をぶち殺せたらいいな ver.2013
明日使えないすごいビット演算

What's hot (20)

PDF
組み込み関数(intrinsic)によるSIMD入門
PDF
constexpr関数はコンパイル時処理。これはいい。実行時が霞んで見える。cpuの嬌声が聞こえてきそうだ
PDF
入門 シェル実装
PDF
MySQLとPostgreSQLの基本的なレプリケーション設定比較
PPTX
BuildKitによる高速でセキュアなイメージビルド
PDF
TVM の紹介
PDF
コンテナの作り方「Dockerは裏方で何をしているのか?」
PDF
とある診断員と色々厄介な脆弱性達
PDF
PostgreSQL 15の新機能を徹底解説
PPTX
純粋関数型アルゴリズム入門
PDF
PostgreSQLアンチパターン
PPTX
DeBERTaV3: Improving DeBERTa using ELECTRA-Style Pre-Training with Gradient-D...
PDF
例外設計における大罪
PDF
Dockerからcontainerdへの移行
PPTX
BigQuery Query Optimization クエリ高速化編
PPTX
Slurmのジョブスケジューリングと実装
PDF
OpenJDKのコミッタってどんなことしたらなったの?解決してきた技術課題の事例から見えてくる必要な知識と技術(JJUG CCC 2023 Spring)
PPTX
30分で分かる!OSの作り方
PDF
プログラムを高速化する話
PDF
[DL輪読会]Attention Is All You Need
組み込み関数(intrinsic)によるSIMD入門
constexpr関数はコンパイル時処理。これはいい。実行時が霞んで見える。cpuの嬌声が聞こえてきそうだ
入門 シェル実装
MySQLとPostgreSQLの基本的なレプリケーション設定比較
BuildKitによる高速でセキュアなイメージビルド
TVM の紹介
コンテナの作り方「Dockerは裏方で何をしているのか?」
とある診断員と色々厄介な脆弱性達
PostgreSQL 15の新機能を徹底解説
純粋関数型アルゴリズム入門
PostgreSQLアンチパターン
DeBERTaV3: Improving DeBERTa using ELECTRA-Style Pre-Training with Gradient-D...
例外設計における大罪
Dockerからcontainerdへの移行
BigQuery Query Optimization クエリ高速化編
Slurmのジョブスケジューリングと実装
OpenJDKのコミッタってどんなことしたらなったの?解決してきた技術課題の事例から見えてくる必要な知識と技術(JJUG CCC 2023 Spring)
30分で分かる!OSの作り方
プログラムを高速化する話
[DL輪読会]Attention Is All You Need
Ad

Viewers also liked (20)

PPTX
自作x86エミュレータの終焉
PPTX
ゼロから始める自作 CPU 入門
PDF
機械語プログラミング
PDF
Web MIDI meets DIY #0
PDF
IPA未踏成果報告会
PDF
Synverll
PDF
Midi with android
PDF
Synthesijer zynq qs_20150316
PDF
エンジニアなら知っておきたい「仮想マシン」のしくみ (BPStudy38)
PPTX
[CB16] バイナリロックスターになる:Binary Ninjaによるプログラム解析入門 by Sophia D’Antoine
PDF
Kpt×ナース(公開版)
PDF
バイナリより低レイヤな話 (プロセッサの心を読み解く) - カーネル/VM探検隊@北陸1
PDF
やってよかったOS作り
PDF
低レイヤー入門
PDF
LLVM最適化のこつ
PPTX
Zynq + Vivado HLS入門
PDF
KPTの理論と実践 公開用 プロジェクトへの「ふりかえりカイゼン」の導入で学んだこと
PDF
レッツゴーディベロッパーX 2014
PDF
KPT採集
PPTX
JSでファミコンエミュレータを作った時の話
自作x86エミュレータの終焉
ゼロから始める自作 CPU 入門
機械語プログラミング
Web MIDI meets DIY #0
IPA未踏成果報告会
Synverll
Midi with android
Synthesijer zynq qs_20150316
エンジニアなら知っておきたい「仮想マシン」のしくみ (BPStudy38)
[CB16] バイナリロックスターになる:Binary Ninjaによるプログラム解析入門 by Sophia D’Antoine
Kpt×ナース(公開版)
バイナリより低レイヤな話 (プロセッサの心を読み解く) - カーネル/VM探検隊@北陸1
やってよかったOS作り
低レイヤー入門
LLVM最適化のこつ
Zynq + Vivado HLS入門
KPTの理論と実践 公開用 プロジェクトへの「ふりかえりカイゼン」の導入で学んだこと
レッツゴーディベロッパーX 2014
KPT採集
JSでファミコンエミュレータを作った時の話
Ad

Similar to Javaで作る超簡易x86エミュレータ (20)

PDF
リバースエンジニアリングのための新しいトレース手法 - PacSec 2010
PPT
Glibc malloc internal
PDF
Lisp Tutorial for Pythonista : Day 3
ODP
Buffer overflow
PDF
セキュアVMの構築 (IntelとAMDの比較、あともうひとつ...) - AVTokyo 2009
PDF
【学習メモ#3rd】12ステップで作る組込みOS自作入門
KEY
core dumpでcode golf
PPT
2008.10.18 L4u Tech Talk
PPT
d-kami x86-2
PPTX
関数型言語&形式的手法セミナー(3)
PDF
ALPSチュートリアル(4) Python入門
PDF
d-kami x86-1
PDF
【学習メモ#4th】12ステップで作る組込みOS自作入門
PPT
LLPML
PDF
Boost Tour 1.50.0 All
PDF
あなたのScalaを爆速にする7つの方法(日本語版)
PDF
Boost Fusion Library
PDF
Haswellサーベイと有限体クラスの紹介
PDF
Minix smp
PDF
Mono is Dead
リバースエンジニアリングのための新しいトレース手法 - PacSec 2010
Glibc malloc internal
Lisp Tutorial for Pythonista : Day 3
Buffer overflow
セキュアVMの構築 (IntelとAMDの比較、あともうひとつ...) - AVTokyo 2009
【学習メモ#3rd】12ステップで作る組込みOS自作入門
core dumpでcode golf
2008.10.18 L4u Tech Talk
d-kami x86-2
関数型言語&形式的手法セミナー(3)
ALPSチュートリアル(4) Python入門
d-kami x86-1
【学習メモ#4th】12ステップで作る組込みOS自作入門
LLPML
Boost Tour 1.50.0 All
あなたのScalaを爆速にする7つの方法(日本語版)
Boost Fusion Library
Haswellサーベイと有限体クラスの紹介
Minix smp
Mono is Dead

Javaで作る超簡易x86エミュレータ

  • 2. 目次 第1章 はじめに 第2章 アセンブラとエミュレータ 第3章 フラグレジスタ 第4章 Hello World 第5章 終わりに 2
  • 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
  • 5. 第2章 アセンブルしよう 2.1 足し算をしてみよう  この章ではアセンブリ言語で簡単なプログラムを作り、機械語だけのファイルと命令と 機械語の組み合わせたもののリストを出力させるようにします。出力した機械語だけの ファイルを実行し、命令と機械語の組み合わせたもののリスト(以下リスト)を読んで、機 械語とアセンブリ言語の命令の対応を学びます。では早速アセンブリ言語と行きたい所で すが、まず学ぶものがあります。それは AX、CX、DX、BX という4つのレジスタです。 これから作るエミュレータは x86 というシリーズの CPU を対象に作り、アセンブリ言 語も x86 用に書きます。その x86 ででてくる値を記憶する場所がレジスタです。よくわ からなければ、アセンブリ言語で使える特殊な変数だと思って進めてください。これらは 16bit のレジスタで 32bit だと EAX のように前に E がまた、64bit だた RAX のよう に R がつきます。今回のプログラムでは AL というレジスタがでてきますが、AL は AX の下位 8 ビットです。上位 8bit は AH というレジスタになっています。 ↓の全体が AX レジスタ AH レジスタ AL レジスタ あと EIP レジスタというのがでてきます。これはプログラムカウンタと言って、現在実 行する命令の位置を指すレジスタです。命令を実行するたびに実行した命令の長さを足し ていきますただし、このレジスタをアセンブリ言語で直接操作することはありませんがエ ミュレータを作るときにでてきます。覚えておきましょう。では、アセンブリ言語を書い てみましょう。その前に作業をするためのフォルダを作り(asm とか名前を付けて)、コ マンドプロンプトでそのフォルダに移動しておきしょう。 ;ここから MOV AL, 0x01 ;AL に1を代入する ADD AL, 0x02 ;AL の値に2を足して AL に入れる fin: JMP fin ;無限ループ ;ここまで これだけです。内容はコメントである;から行の終わりまでを見ればわかると思います。 AL に1を入れて、2を足したものを AL に入れています。結果 AL には3が入っていま す。これをアセンブルします。ファイル名は add.asm として上記内容を保存し、以下 のコマンドを打ちます。 5
  • 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
  • 25. while(true){ emulator.execute(); } }catch(IOException e){ System.out.println("ファイルの読み込みに失敗しました。"); e.printStackTrace(); }catch(RuntimeException e){ emulator.dumpRegisters(); e.printStackTrace(); } } //ここまで これをコンパイルしたら以下のコマンドで HelloWorld を実行します。 Java Emulator HelloWorld そうすると java.lang.RuntimeException: Not Implemented 0x30 という表 示が途中にでると思います。これはオペコード 0x30 を実装してないよ!という意味に なります。マニュアルで 0x30 を探してみましょう。するとマニュアル B で XOR が見 つかります。r/m8 と r8 の XOR を取ると書いてあります。r/m8 と r8 ですが、これは ModR/M バイトというオペコードの後ろに続く値があります。これは r/m と r(もしく はオペコード)というように分解できます。実際には3分割されていて1バイトの最初の 2bit が mod 部、真ん中の 3bit が r 部、最後の 3bit が r/m 部です。もし ModR/M が F3 だった場合、2進数で 11110011 になりますが、これは以下のように分解すること ができます。mod 部と r/m 部はセットで r/m8 のような表記になっています。 mod r(もしくはオペコード) r/m 11 110 011 そしてマニュアル A の 35 ページにこの値と使われるレジスタ、もしくはメモリの番地の 割り当てが載っています。例えば上の例だと mod 部が 11 で r/m 部が 011 なので r/m は EBX、BX、BL、MM3、XMM3 のどれかということになります。これは命令とアド レスサイズによって特定されます。今回の 0x30 で上の例だと BL が選ばれます。そして r は 011 なので r8(8 は 8bit の意味)の場合 DH が選ばれます。これが r16 となってる 場合は SI が選ばれます。なので上の例の場合 XOR だと BL と DH の XOR ということに なります。 では今回の XOR の後ろに続く ModR/M を確認するために以下のメソッド を Emulator クラスに追加します。 25
  • 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