マイペースなプログラミング日記

x86エミュレータやFPGA、WebGLにお熱なd-kamiがマイペースに書くブログ

Java Advent Calendar 2011 Javaでx86エミュレータを作ろう!超入門編

このエントリはJava Advent Calendar 2011に向けて書かれたものです
前に『Javaで作る超簡易x86エミュレータ』を公開したばかりだが、またこのネタを使う。64bitはどうなってるかわからないので、このエントリでは32bitを扱います。まず最低限のクラスと変数を用意します。必要なのは汎用レジスタとメモリとプログラムカウンタ。メモリにプログラムを読み込んで、プログラムカウンタの指してる番地の命令を実行して、レジスタやメモリを操作していきます。とりあえずクラス作成。registersが汎用レジスタの配列。memoryがメモリ、EIP(Instruction Pointerの拡張版?)がプログラムカウンタとなります。

public class Emulator{
    //レジスタ郡
    private int[] registers;

    //メモリ
    private byte[] memory;

    //プログラムカウンタ
    private int eip;
}

まずコンストラクタを追加します。メモリの容量を受け取ってmemoryを初期化します。あとregistersも初期化しておきます。x86の汎用レジスタの個数は8つです(多分)。

public Emulator(int memorySize){
    registers = new int[8];
    memory = new byte[memorySize];
}

このクラスにとりあえずファイルを読み込む処理を加えます。ファイルの内容をmemoryに入れます。BufferedInputStreamとFileInputStreamとIOExceptionのimportも忘れずに

public void load(String fileName) throws IOException{
    BufferedInputStream in = new BufferedInputStream(new FileInputStream(fileName));
    in.read(memory);

    in.close();
}

それで、今回読み込むファイルは以下のアセンブリをnasmでアセンブルしたものを使います。以下のリンクからアセンブリしたものをダウンロードできます。
add 直

    MOV AL, 0x01
    ADD AL, 0x02

fin:
    JMP fin

これを読み込ませるためにmainメソッドをEmulatorに使いします。メモリの容量は今回のプログラムでは512バイトあればいいです。が、一応1Mぐらい確保しておきます。

public static void main(String[] args) throws IOException{
    Emulator emulator = new Emulator(1 * 1024 * 1024);
    emulator.load("add");
}

これでaddを読み込めたので、これを実行していきます。このファイルの内容を16進数で表すとこうなっています。おまじないの関係で実はもっと続いていますが、ここの部分だけ実行すればOKです。

B0 01 04 02 EB FE

この16進数で表されたプログラムを先頭から実行していきます。そのために今回はプログラムカウンタを0から始めています。とりあえず実行用のメソッドを用意しておきます

public void execute(){
    int code = memory[eip] & 0xFF;
}

このcodeに先ほどの0xB0とかが入っています。&を使っている理由はJavaのbyte型は符号付なのですが、x86は基本的に符号無しの値を使うため&を使って符号無しの値にしています。最初は0xB0が入ってるのでそれを実行します。0xB0はMOV命令です。0xB0はALレジスタにこの命令の後ろに続く1byteを代入するというものです。ALレジスタはregisters[0]が表しているのでregisters[0]にmemoryのeip + 1番目を代入すればOKです。この命令が終わったらプログラムカウンタを動かします。動かす量は命令の長さ(今回は1byte)とデータの長さ(今回は1byte)分移動させます。今回は+2しておきます。

public void execute(){
    int code = memory[eip] & 0xFF;
        
    if(code == 0xB0){
        registers[0] = (memory[eip + 1] & 0xFF)
        eip += 2;
    }
}

次の命令に移ります。次の命令は04です。この命令はADDです。04はALにこの命令の後ろにある1byteを足します。この命令実行後プログラムカウンタは+2されます。

public void execute(){
    int code = memory[eip] & 0xFF;
        
    if(code == 0xB0){
        registers[0] = (memory[eip + 1] & 0xFF)
        eip += 2;
    }else if(code == 0x04){
        registers[0] += (memory[eip + 1] & 0xFF)
        eip += 2;
    }
}

最後にEBですが、これはJMP命令です。プログラムカウンタの値を増減するものです。この命令の後ろの符号付きの値をプログラムカウンタに足します。今回は無限ループに使うため、executeを実行させ続けると無限ループに入ります。とりあえずコードです。

public void execute(){
    int code = memory[eip] & 0xFF;
        
    if(code == 0xB0){
        registers[0] = (memory[eip + 1] & 0xFF);
        eip += 2;
    }else if(code == 0x04){
        registers[0] += (memory[eip + 1] & 0xFF);
        eip += 2;
    }else if(code == 0xEB){
        eip += memory[eip + 1];
        eip += 2;
    }
}

これで今回のプログラムで使う命令は全て対応しました。しかし、このままだと何も表示が行われずに何が起こっているのかわからないため、レジスタの値とプログラムカウンタの値を表示するメソッドを追加します。

public void dump(){
    for(int i = 0; i < registers.length; i++){
        System.out.printf("Register[%d] = %x\n", i, registers[i]);
    }
        
    System.out.println();
    System.out.printf("EIP = %x\n", eip);
}

では、mainメソッドからexecuteを呼び出し、結果をdumpしていきましょう。

public static void main(String[] args) throws IOException{
    Emulator emulator = new Emulator(1 * 1024 * 1024);
    emulator.load("add");
        
    for(int i = 0; i < 3; i++){
        emulator.execute();
    }
        
    emulator.dump();
}

これをコンパイルして実行するとRegister[0]が3で他のレジスタが0になっていて、そしてEIPが4になっていると思います。これはRegister[0]に1を代入してさらに2を足した結果となっています。EIPはJMP命令を実行した結果4となり、この後、何回executeを実行しても4となります。これが無限ループには入った証拠となります。

以上で今回のエントリは終了です。実際のエミュレータはもっと難しいものですが、それを説明してるとこんなエントリでは説明しきれないため、かなり省いています。それでもx86バイナリを実行してるのは事実だと思うのでエミュレータと名乗らせてもらいます。最後に今回Javaで書いたソースコードをまとめておきます。

import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;

public class Emulator{
    //レジスタ郡
    private int[] registers;

    //メモリ
    private byte[] memory;

    //プログラムカウンタ
    private int eip;
    
    public Emulator(int memorySize){
        registers = new int[8];
        memory = new byte[memorySize];
    }
    
    public void load(String fileName) throws IOException{
        BufferedInputStream in = new BufferedInputStream(new FileInputStream(fileName));
        in.read(memory);

        in.close();
    }
    
    public void execute(){
        int code = memory[eip] & 0xFF;
        
        if(code == 0xB0){
            registers[0] = (memory[eip + 1] & 0xFF);
            eip += 2;
        }else if(code == 0x04){
            registers[0] += (memory[eip + 1] & 0xFF);
            eip += 2;
        }else if(code == 0xEB){
            eip += memory[eip + 1];
            eip += 2;
        }
    }
    
    public void dump(){
        for(int i = 0; i < registers.length; i++){
            System.out.printf("Register[%d] = %x\n", i, registers[i]);
        }
        
        System.out.println();
        System.out.printf("EIP = %x\n", eip);
    }
    
    public static void main(String[] args) throws IOException{
        Emulator emulator = new Emulator(1 * 1024 * 1024);
        emulator.load("add");
        
        for(int i = 0; i < 3; i++){
            emulator.execute();
        }
        
        emulator.dump();
    }
}