[Java]〈Hello World〉をバイナリエディタだけで使って出力させてみた

釘宮愼之介
135

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

こんにちは。英語サプリの開発チームに所属しているAndroidエンジニアの@kgmyshinです。

突然ですがJavaで〈Hello World〉と表示するコードは下記となります。

class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

本来であればこのコードをコンパイルしてHello.classを作り、そしてそれを実行することによって〈Hello World〉が表示されます。

この記事では、その〈このコードをコンパイルしてHello.classを作る〉という部分を 手動で行う 方法をご紹介します。

なぜするのか

私自身としては「興味があったから」というのが回答なのですが、クラスファイルを手動で作成することには多くのメリットがあります。

まず クラスファイルの構造への理解が深まります。そして、それによってコンパイラが何をするのか、逆コンパイラが何をするのかへの想像がつくようになります。さらにはJVM系言語の作成もできるようになるかもしれません。

こういうメリットに魅力を感じる方は是非本記事を読み進めてみてください!

まずはクラスファイルの構造を知る

クラスファイルの構造は下記のようになっております。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

u2u4cp_infomethod_infoは型です。

特にu2は〈ビッグエンディアンバイト順の符号なし16ビット整数〉で、u4は〈ビッグエンディアンバイト順の符号なし32ビット整数〉です。つまりu2は2byte、u4は4byteということを表しています。

次に各要素について一つ一つ簡単な説明をしていきます。

要素 説明
magic ここにはマジックナンバーを指定します。
minor_version クラスファイルフォーマットのマイナーバージョンです。
major_version クラスファイルフォーマットのメジャーバージョンです。
constant_pool_count constant_poolの数に1を足したものになります。
constant_pool 定数プールです。ここでは複数のクラス名やメソッド名、文字列などを定義します。
access_flags ここではクラスあるいはインタフェースの情報やアクセス制御に関するフラグが設定されます。
this_class 名前通りこのクラスあるいはインタフェースが何なのかという情報が格納されます。型はu2で、constant_poolテーブルで定義されてるはずのこのクラス情報のインデックス番号(何番目かという値)が入ります。
super_class このクラスの親クラスを示すconstant_poolテーブルのインデックス番号が格納されます。
interfaces_count interfacesの数です。
interfaces このクラスが実装してしているインタフェース情報です。定数プールに定義されているインタフェースのインデックス番号が格納されます。
fields_count fieldsの数です。
fields 各フィールドの定義情報です。
methods_count methodsの数です。
methods このクラスで定義されている各メソッド情報です。
attributes_count attributesの数です。
attributes attributeは上記以外の付加情報です。ソースファイル名であったりインナークラス情報であったり、実際のロジックなどが格納されます。

さっそく書いてみる

さっそくバイナリエディタを開いて書いていきましょう1)私はmacを使っているので0xEDを使いました。

1. magicを設定する

magicには必ずCAFEBABEを設定します。

21ae55505a9153fee1bdbe4c61b137f4

なぜCAFEBABEなのかというのは理由にはちょっとした逸話があります。こういう逸話を知っていると、とても覚えやすいので読んでみることをお勧めします。

2. minor_version, major_versionを指定する

今回はJava SE 8を使うのでminor versionを0x0000、major versionを0x0034とします。

8ed367b7d301d1c63fc459ba05f8356d

3. constant_pool_count, constant_poolを設定する

さっそくconsntat_poolに情報を設定していきます。

定数プールの型は下記のようになっております。

cp_info {
    u1 tag;
    u1 info[];
}

まず何の定数なのかを示すtagがあり、その後にtagにしたがった情報が格納されます。

3.1 クラスを定義する

3.1.1 メインクラスを定義する

まずはこのクラスのクラス名を定義します。文字列を定義するにはCONSTANT_Utf8_infoを作ります。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

tagは0x01、lengthには文字列の長さ、bytesに文字列を設定します。今回はHelloというクラス名なのでlengthは0x05、bytesはHelloをbyte変換したもの01 00 05 48 65 6c 6c 6fが格納されます。

そしてCONSTANT_Class_infoを定義します。

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

tagは0x07、name_indexにはこのクラスのクラス名が定義されている定数プールのインデックス番号、つまり先ほど定義したHelloのインデックス番号0x01を指定します。

と、このように考えていくのですが少々煩わしいので以降は、一旦どういったバイナリになるかやtagなどの確定事項は最後に確認するものとして、下記のようにインデックス番号と型、そして値のみを考えていくとします。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
3.1.2 スーパークラスを定義する

HelloクラスはObjectクラスを継承していますのでこれも定義しておきます。まずは文字列から定義します。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object

パッケージの区切りである.はクラスファイル内では/で表します。そしてクラス情報を定義します。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
3.1.3 使用するクラスを定義する

Helloクラス内で使われているSystemとoutの型であるPrintStreamを定義しましょう。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7

3.2 フィールドを定義する

3.2.1 使用するフィールドを定義する

Systemのstaticなフィールドであるoutが使用されているので、これを定数プールに定義しておきます。フィールド情報の型は下記のようになっております。

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

class_indexには先ほど定義したPrintStreamのインデックス番号を指定します。name_and_type_indexはまだ未定義なので定義します。型は以下です。

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

nameにはoutを、descriptorにはLjava/io/PrintStream;というインスタンス情報を格納します。Lで型を挟むことでインスタンスを表しています。

以上からフィールド情報を定義すると下記のようになります。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11

3.3 メソッドを定義する

3.3.1 使用するメソッドを定義する

まずはprintlnを定義しましょう。メソッドの型は下記です。

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

内容はフィールドの時とほぼ同じです。

CONSTANT_NameAndType_infoを定義します。
nameはprintlnで、descriptorは(Ljava/lang/String;)Vとします。()内は引数でVはvoidを表しています。

以上からメソッドを定義すると

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11
#13   = Utf8         println
#14   = UTf8         (Ljava/lang/String;)V
#15   = NameAndType  #13:#14
#16   = Methodref    #8.#15

となります。

次はスーパークラスのコンストラクタを定義します。コンストラクタは特殊でnameは〈init〉とします。またdescriptorは引数なしのため()Vとします。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11
#13   = Utf8         println
#14   = UTf8         (Ljava/lang/String;)V
#15   = NameAndType  #13:#14
#16   = Methodref    #8.#15
#17   = Utf8         <init>
#18   = Utf8         ()V
#19   = NameAndType  #17:#18
#20   = Methodref    #3.#19

3.4 文字列を定義する

表示させる文字列Hello, Worldを定義します。型は下記です。

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

定義するとこうなります。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11
#13   = Utf8         println
#14   = UTf8         (Ljava/lang/String;)V
#15   = NameAndType  #13:#14
#16   = Methodref    #8.#15
#17   = Utf8         <init>
#18   = Utf8         ()V
#19   = NameAndType  #17:#18
#20   = Methodref    #3.#19
#21   = Utf8         Hello, World
#22   = String       #21

あとから追加する可能性はありますが、ここで一旦定数プールの作成は終わりにします。書き出すとこのようになります。

bbe6a1960a08e344f41354b1e9a0f08e

tagの後にインデックス番号もしくはlengthと可変なものが続くの単純な構造なので見分けがついてきますね。

4. access_flagsを設定する

Java SE 8以降すべてのクラスでACC_SUPER(0x0020)が設定されます。今回はJava SE 8の環境で動かすのでひとまずこれを設定しておきます。

a7f94409166a69aadfbe695fa224163f

5. this_class, super_classを設定する

this_classはこのクラスファイルのインデックス番号なので、#2となります。そしてこのクラスのスーパークラスはObjectクラスです。super_classには定数プールでのObjectクラス#4を設定します。

3fe91ba5593c8137d9a2f826135f84fb

6. interfaces_count, interfacesを設定する

このクラスにおいて定義されるインタフェースはありません。そのため、interfaces_countは0x0000、interfacesはなしです。

a66d082e8fa4d1d3df17a1e03c4c982e

7. fields_count, fieldsを設定する

このクラスにおいて定義されるフィールドはありません。そのため、fields_countは0x0000、fieldsはなしです。

52c903644da7ae8e39076e93a528176e

8. methods_count, methodsを設定する

8.1 デフォルトコンストラクタを設定する

methodの型は下記のようになっております。

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

まずaccess_flagsですが、設定するものがないので0x0000とします。次にname_indexには<init>のインデックス番号である#17、descriptor_indexにはV()である#18を設定しておきましょう。次にattributesを設定しましょう。ここにメソッドのロジックそのものなどを記述していきます。

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

さっそく、メソッドの内容であるCodeを設定していきます。attribute_name_indexには定数プールのインデックス番号を設定しなければならないのですが、現状は未定義ですので追加します。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #7
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11
#13   = Utf8         println
#14   = UTf8         (Ljava/lang/String;)V
#15   = NameAndType  #13:#14
#16   = Methodref    #8.#15
#17   = Utf8         <init>
#18   = Utf8         ()V
#19   = NameAndType  #17:#18
#20   = Methodref    #3.#19
#21   = Utf8         Hello, World
#22   = String       #21
#23   = Utf8         Code

追加が完了したら、attribute_name_indexには#23を設定しておきましょう。
attributeがCodeの場合、型は下記になります。

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

attribute_lengthは先頭の6byte分以降の長さです。そのため、これは最後に設定します。

次にmax_stack, max_locals。max_stackは0x001を、max_localsにも0x001を設定します。code_lengthはこの次のcodeの長さなので、codeを定義したのちに設定します。

次にcodeです。ここにオペコードを書いていきます。

コンストラクタの場合、処理は下記になります。

  1. (0x2a) aload_0 (thisをstackに乗せる)
  2. (0xb7 0x00 0x14)invokespecial #20 (デフォルトコンストラクタを実行する)
  3. (0xb1) return (リターンする)

よって2A B7 14 B1となり、code_lengthは0x00000004となります。例外処理はしていないのでexception_table_lengthは0x0000でexception_tableはなし。また付加情報はないのでattributes_countは0x0000でattributesもなしです。

8.2 main関数を設定する

デフォルトコンストラクタとすることはほとんど同じです。main関数の名前及びdescriptorは未定義なので定数プールに追加します。

番号   型           値
#1    = Utf8         Hello
#2    = Class        #1
#3    = Utf8         java/lang/Object
#4    = Class        #3
#5    = Utf8         java/lang/System
#6    = Class        #5
#7    = Utf8         java/io/PrintStream
#8    = Class        #6
#9    = Utf8         out
#10   = Utf8         Ljava/io/PrintStream;
#11   = NameAndType  #9:#10
#12   = Fieldref     #8.#11
#13   = Utf8         println
#14   = UTf8         (Ljava/lang/String;)V
#15   = NameAndType  #13:#14
#16   = Methodref    #7.#15
#17   = Utf8         <init>
#18   = Utf8         ()V
#19   = NameAndType  #17:#18
#20   = Methodref    #3.#19
#21   = Utf8         Hello, World
#22   = String       #21
#23   = Utf8         Code
#24   = Utf8         main
#25   = Utf8         ([Ljava/lang/String;)V

[は配列を表します。

次に、オペコード。main関数のオペコードは下記です。オペコードは下記のようになります。

  1. (0xb2 0x00 0x0C) getstatic #12 (outをstackに乗せる)
  2. (0x12 0x15) ldc #21 ("Hello, World"という文字列の参照値をstackに乗せる)
  3. (0xb6 0x00 0x10) invokevirtual #16 (printlnを実行する)
  4. (0xb1) return  (リターンする)

それ以外についてはデフォルトコンストラクタとほぼ同様なので省略します。

c833960122ea14b3e73b16bea1a7d0c6

9. attributes_count, attributesを設定する

付加情報はなしです。そのためattributes_countは0x0000、attributesはなしです。

947516addcdbf6a667a0be41ae211f87

実行してみる

スクリーンショット 2015-12-06 17.29.17

なんとか無事「Hello, World」と表示されました!

所感

やっぱり手動でやるべきことではないというのが第一の感想なのですが、ただ1度やるだけで単純にjavapコマンドの出力と仕様書を眺めて理解するよりは、より早く深く理解することができたかなと実感しています。機会があればkotlinなどのコンパイラなどもじっくり眺めてみようかなと思いました。

おまけ

次のリンクのようにメモを取りながら書くと楽でした。

脚注   [ + ]

1. 私はmacを使っているので0xEDを使いました。