[Java]〈Hello World〉をバイナリエディタだけで使って出力させてみた
釘宮愼之介
この記事は 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];
}
u2
やu4
、cp_info
、method_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
を設定します。
なぜCAFEBABE
なのかというのは理由にはちょっとした逸話があります。こういう逸話を知っていると、とても覚えやすいので読んでみることをお勧めします。
2. minor_version, major_versionを指定する
今回はJava SE 8を使うのでminor versionを0x0000
、major versionを0x0034
とします。
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
あとから追加する可能性はありますが、ここで一旦定数プールの作成は終わりにします。書き出すとこのようになります。
tagの後にインデックス番号もしくはlengthと可変なものが続くの単純な構造なので見分けがついてきますね。
4. access_flagsを設定する
Java SE 8以降すべてのクラスでACC_SUPER(0x0020)が設定されます。今回はJava SE 8の環境で動かすのでひとまずこれを設定しておきます。
5. this_class, super_classを設定する
this_classはこのクラスファイルのインデックス番号なので、#2
となります。そしてこのクラスのスーパークラスはObjectクラスです。super_classには定数プールでのObjectクラス#4
を設定します。
6. interfaces_count, interfacesを設定する
このクラスにおいて定義されるインタフェースはありません。そのため、interfaces_countは0x0000
、interfacesはなしです。
7. fields_count, fieldsを設定する
このクラスにおいて定義されるフィールドはありません。そのため、fields_countは0x0000
、fieldsはなしです。
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です。ここにオペコードを書いていきます。
コンストラクタの場合、処理は下記になります。
- (0x2a) aload_0 (thisをstackに乗せる)
- (0xb7 0x00 0x14)invokespecial #20 (デフォルトコンストラクタを実行する)
- (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関数のオペコードは下記です。オペコードは下記のようになります。
- (0xb2 0x00 0x0C) getstatic #12 (outをstackに乗せる)
- (0x12 0x15) ldc #21 ("Hello, World"という文字列の参照値をstackに乗せる)
- (0xb6 0x00 0x10) invokevirtual #16 (printlnを実行する)
- (0xb1) return (リターンする)
それ以外についてはデフォルトコンストラクタとほぼ同様なので省略します。
9. attributes_count, attributesを設定する
付加情報はなしです。そのためattributes_countは0x0000
、attributesはなしです。
実行してみる
なんとか無事「Hello, World」と表示されました!
所感
やっぱり手動でやるべきことではないというのが第一の感想なのですが、ただ1度やるだけで単純にjavap
コマンドの出力と仕様書を眺めて理解するよりは、より早く深く理解することができたかなと実感しています。機会があればkotlinなどのコンパイラなどもじっくり眺めてみようかなと思いました。
おまけ
次のリンクのようにメモを取りながら書くと楽でした。