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

DTMやプログラミングにお熱なd-kamiがマイペースに書くブログ

カーネル/VM Advent Calendar 14日目: Javaで書くブートローダ入門

この記事はカーネル/VM Advent Calendarのネタとして書きました。

さて、今回はタイトルの通りJavaで書くブートローダです。すでに過去のエントリで書いてたりしますが、使いまわします(おい。まぁ、ブートローダと言ってもまだディスクの読み込みもできてないので、ブートセクタで動くプログラム程度の物です。でもJavaで書きます。あえてJavaで書きます。やり方は

という流れです。まずJavaでプログラムを書きます。まずBIOS.javaです。とりあえずメソッドだけを定義しておきます。putCharacterメソッドはこの段階では何もしません。これはコンパイラにこんなメソッドがあるんだよ!と伝えるためだけのクラスです。

public class BIOS{
    public static void putCharacter(char c){}
}

次にプログラム本体です。先程作ったBIOS.putCharacterを呼び出しています。ただ、これを素直にコンパイルして、実行しても何も起こりません。

public class Hello{
    public static void main(String[] args){
        BIOS.putCharacter('H');
    }
}

そこで次の段階に入ります。今度は先ほどのファイルをコンパイルしてできたHello.classを解析します。クラスファイルの内容まで説明するのも大変なので、バイトコードだけ載せておきます。Hello.classのmainの中身はこうなっています。全部16進数です。

10 48 B8 00 02 B1

これをx86バイナリに変換していきます。以下にでてくるバイトコードの説明を書いておきます。

  • 0x10 bipush
    • この命令の次にある値をスタックに乗せる。今回の場合0x48をスタックに乗せます
  • B8 invokestatic
    • この命令の後に続く2つの値を使ってコンスタントプールからstaticなメソッドを取り出し呼び出す。staticなメソッドを呼び出すことがわかればOK。今回はBIOS.puCharacterを呼び出しています
  • B1 return
    • 今回は無視、ブートローダのメインプログラムでリターンされても困る。変わりに最後に無限ループを入れておく。

以上が今回扱うバイトコードの命令です。次に上記のバイトコードがどのx86の命令に対応させるかを書きます。

  • 0x10 bipush
    • x86にあるPUSH命令をそのまま使える。オペコードは0x6A
  • B8 invokestatiic
    • 今回はBIOS呼び出しなので、INT命令を使う、オペコードは0xCD。文字表示を行うためのレジスタ設定や、引数である文字がスタックに積まれてるので、それをPOPする必要がある
  • B1 return
    • 無視

プログラムの最後に無限ループ用のJMP命令を入れておく。無限ループさせるには0xEB 0xFEと続ければ良い。最後に今回はフロッピブートを想定しているので、511バイト目と512バイト目におまじないが必要。0x55 0xAAを書き込んでおきましょう。

変換プログラムはこんな感じ

import parser.ClassParser;
import parser.struct.StaticMethod;

import java.io.FileOutputStream;
import java.io.BufferedOutputStream;

public class Class2x86{
    private byte[] memory;
    private int index;
    
    public Class2x86(){
        memory = new byte[512];
        index = 0;
        
        memory[510] = (byte)0x55;
        memory[511] = (byte)0xAA;
    }
    
    public static void main(String[] args) throws Exception{
        Class2x86 translator = new Class2x86();
        translator.translate(args[0], args[1]);
    }
    
    public void translate(String className, String x86Name) throws Exception{
        ClassParser parser = new ClassParser();
        parser.parse(className);
        byte[] code = parser.getMethodCode("main");
        
        for(int i = 0; i < code.length; i++){
            int opcode = code[i] & 0xFF;
            
            if(opcode == 0x10){
                put((byte)0x6A);
                put(code[i + 1]);
                i++;
            }else if(opcode == 0xB8){
                int pindex = (code[i + 1] & 0xFF) << 8;
                pindex |= (code[i + 2] & 0xFF);
                StaticMethod method = parser.getStaticMethod(pindex);
                String name = String.format("%s.%s", method.getClassName(), method.getMethodName());

                if("BIOS.put".equals(name)){
                    put((byte)0x58);
                    put((byte)0xB4);
                    put((byte)0x0E);
                    put((byte)0xB7);
                    put((byte)0x00);
                    put((byte)0xB3);
                    put((byte)0x15);
                    put((byte)0xCD);
                    put((byte)0x10);
                    
                    i+=2;
                }
            }
        }
        
        put((byte)0xEB);
        put((byte)0xFE);
        
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(x86Name));
        bos.write(memory);
        bos.close();
    }
    
    public void put(byte data){
        memory[index] = data;
        index++;
    }
}

あとは、このプログラムに先程コンパイルしてできたHello.classを読み込ませるのみ。qemuやらbochsやらで変換後のプログラムを読み込ませるとHが表示されます。めでたしめでたし。次回のカーネル/VM Advent Calendarは期待のmasami256さんです。期待して待ちましょう