初心にもどってJava ~キサマはHello Worldを嘗めたッッッ~

Table of Content

キサマはHello Worldを嘗めたッッッ

プログラミング言語を始める場合に「Hello World」と出力することはよくあります。
特に業務で使わないといけなくなった場合だとゴキブリダッシュばりの速度でHelloWorldを書いて次の段階に進もうとします。

だが、私はこのHello Worldが、どう動いていたのか理解していたのでしょうか。
今回は、烈先生に「キサマはHello Worldを嘗めたッッッ」とボコられない程度に初心に戻ってHello Worldを見なおしてみようと思います。

ゴキブリダッシュ的 Hello World

環境

Windows 10
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

Helloworldの作成~実行

いつものようにHello Worldを作成して実行します。
まずテキストエディタでjavaのコードを作成します。

HelloWorld.java

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

作成したjavaファイルを入力としてjavacコマンドを実行してclassファイルを作成します。
今回は「-g:none」オプションを付けてデバッグ情報は生成しないようにします。

javac -g:none HelloWorld.java

作成した「HelloWorld.class」をjavaコマンドで実行します。

>java HelloWorld
Hello World

「Hello World」と出力されました。
cやc++で作成した実行ファイルと異なり、作成したHelloWorld.classはJavaが入っている環境であれば、WindowsでもMacでもLinuxでも同様に動作します。
いやー、Javaって便利ですね。

終わり!!閉廷!!以上!!皆解散!!

そんなふうに考えていた時期が俺にもありました

いままではそれで終わらせてました。
そう、このHelloWorld.classがどういうファイルなのかを考える機会がなかったのです。
今回はせっかくなので、作成されたバイナリファイルの内容を確認してみます。

HelloWorld.classの中身は以下のようなバイナリになっています。

image.png

このバイナリファイルを読み解くには、「The Java® Virtual Machine Specification」を読む必要があります。
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

今回使用するJVM仕様

今回使用するJVMの仕様を簡単に記載しますが、さっさとクラスファイルのバイナリを解析をしたい人はスキップしてください。

classファイルの構造は以下の通りです。

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];
}

u1は1バイト、u2は2バイト、u4は4バイトのデータを表します。
その他については構造体なので都度説明します。

magic はクラスファイルフォーマットを識別するマジックナンバーで、「0xCAFEBABE」です。
なお、このマジックナンバーの由来はCAFEBABE: Java's Magic Wordに記載されているので、一読しておくとデートの時に使ってドン引かれます。

minor_version、major_versionはこのクラスファイルのマイナーバージョン番号とメジャーバージョン番号です。メジャーバージョン番号とマイナーバージョン番号によって、クラスファイル形式のバージョンが決まります。

constant_pool[]とconstant_pool_countは文字列定数、クラス名およびインタフェース名、フィールド名、その他の定数を表す構造体のテーブルとその数です。
constant_poolへのインデックスは1~constant_pool_count-1が有効範囲になります。

access_flagsは以下のフラグの組み合わせになります。

フラグ名 解釈
ACC_PUBLIC 0x0001 publicが宣言されています。そのパッケージの外部からアクセスすることができます。
ACC_FINAL 0x0010 finalが宣言されています。サブクラスは許可されていません。
ACC_SUPER 0x0020 invokespecial命令によって呼び出されたときに、スーパークラスメソッドを特別に扱います。
ACC_INTERFACE 0x0200 クラスではなくインタフェースです。
ACC_ABSTRACT 0x0400 abstractを宣言されています。インスタンス化してはいけません。
ACC_SYNTHETIC 0x1000 コンパイラによって生成されたものであることをしめします。※例えばクラス内にクラスを作成したときに作成されるクラスファイル(ex. Hello$Test.class)に付与されていました
ACC_ANNOTATION 0x2000 アノテーションとして宣言されています。
ACC_ENUM 0x4000 enum型として宣言されています。

this_class はconstant_pool[]中の有効なインデックス値である必要があります。インデックスで参照されるデータは当該ファイルで指定されたクラスの情報を保持するCONSTANT_Class_info構造体である必要があります。

super_class は0またはconstant_pool[]中の有効なインデックス値である必要があります。インデックスで参照されるデータはこのクラスファイルで定義されたクラスの直接のスーパークラスを表すCONSTANT_Class_info構造体である必要があります。

interfaces_count、interfaces[]はこのクラスファイルで定義されているクラスのインターフェイスを表すCONSTANT_Class_info構造体へのインデックスの配列になっています。インターフェイスがない場合はinterfaces_countは0となり、interfaces[]は存在しません。

fields_count、fields[]はこのクラスファイルで定義されているクラスのフィールドを表すfield構造体の配列になっています。フィールドがない場合はfields_countは0となり、fields[]は存在しません。

methods_count、methods[]はこのクラスファイルで定義されているクラスのメソッドを表すmethod構造体の配列になっています。フィールドがない場合はmethods_countは0となり、methods[]は存在しません。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

constant_pool

この構造体は1バイトのタグでどのような構造体になるかが決定されます。

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

以下で今回使用する分は説明するので、その他の構造体については以下を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

CONSTANT_Class

クラスまたはインタフェースを表すために使用されます。

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

tagにはCONSTANT_Classをあらわす7が格納されます。
name_index項目の値は constant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスになります。

CONSTANT_Fieldref

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

tagにはCONSTANT_Fieldref_info値をあらわす9が格納されます。
class_indexの値はconstant_poolテーブル中のCONSTANT_Class_info構造体へのインデックスです。
name_and_type_index項目 の値はconstant_poolテーブル中のCONSTANT_NameAndType_info構造体へのインデックスです。

CONSTANT_Methodref

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

tagにはCONSTANT_Methodref値をあらわす10が格納されます。
class_indexの値はconstant_poolテーブル中のCONSTANT_Class_info構造体へのインデックスです。
name_and_type_index項目 の値はconstant_poolテーブル中のCONSTANT_NameAndType_info構造体へのインデックスです。

CONSTANT_String

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

tagにはCONSTANT_String値をあらわす8が格納されます。
string_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。

CONSTANT_NameAndType

フィールドまたはメソッドを表すために使用されます。ただし、それが属するクラスまたはインタフェースの型は示されません。

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

tagにはCONSTANT_NameAndType値をあらわす12が格納されます。
name_index項目の値はconstant_pool中のCONSTANT_Utf8_info構造体への有効なインデックスでなければなりません。

CONSTANT_Utf8

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

tagにはCONSTANT_Utf8値をあらわす1が格納されます。
lengthにはbytes配列内のバイト数を表します(文字列の長さではありません)
byte配列には文字列のバイトが含まれます。また終端文字は含まれません。この文字列の詳細については下記を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.7

method構造体

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

access_flags項目の値はこのメソッドへのアクセス許可とこのメソッドのプロパティを示すために使用されるフラグの組み合わせです。

フラグ名 説明
ACC_PUBLIC 0x0001 publicを宣言されました。そのパッケージの外部からアクセスすることができます。
ACC_PRIVATE 0x0002 privateを宣言しました。定義クラス内でのみアクセス可能です。
ACC_PROTECTED 0x0004 protectedを宣言しました。サブクラス内でアクセスできます。
ACC_STATIC 0x0008 staticを宣言しました。
ACC_FINAL 0x0010 finalを宣言しました。上書きされてはならない。
ACC_SYNCHRONIZED 0x0020 synchronizedを宣言しました。
ACC_BRIDGE 0x0040 Javaプログラミング言語用のコンパイラによって生成されたブリッジメソッドを示すために使用されます。Java Generics - Bridge method?を参照してください。
ACC_VARARGS 0x0080 可変数の引数で宣言されています。
ACC_NATIVE 0x0100 nativeを宣言しました。Java以外の言語で実装されている。
ACC_ABSTRACT 0x0400 abstractを宣言しました。実装は提供されていません。
ACC_STRICT 0x0800 strictfpを宣言しました。
ACC_SYNTHETIC 0x1000 コンパイラによって生成されたソースコードに表示されないことを示しています。

name_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。メソッド名またはまたはが格納されています。

descriptor_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。メソッド記述子が格納されています。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

attribute構造体

この構造体は属性によって構造体の形が変わります。共通的な形式は以下のようになります。

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

attribute_name_indexの値はconstant_poolテーブル中のCONSTANT_Utf8_info構造体へのインデックスです。
attribute_lengthは後続の情報の長さをバイト数であらわします。
infoについては属性ごとにことなります。

属性 Location
SourceFile ClassFile
InnerClasses ClassFile
EnclosingMethod ClassFile
SourceDebugExtension ClassFile
BootstrapMethods ClassFile
ConstantValue field_info
Code method_info
Exceptions method_info
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations method_info 49.0
AnnotationDefault method_info
MethodParameters method_info
Synthetic ClassFile, field_info, method_info
Deprecated ClassFile, field_info, method_info
Signature ClassFile, field_info, method_info
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations ClassFile, field_info, method_info
LineNumberTable Code
LocalVariableTable Code
LocalVariableTypeTable Code
StackMapTable Code
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code

ここで説明しない項目については下記を参照してください。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7

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_name_indexとattribute_lengthは共通の形式で説明したものです。
attribute_name_indexで指定された文字は「Code」である必要があります。

max_stack項目の値は、 このメソッドのオペランドスタックの最大の深さになります。

max_locals項目の値は、このメソッドの呼出し時に割り当てられたローカル変数の数です。

code_length項目の値は、code[]の数となります。

code配列は、メソッドを実装するJava仮想マシン・コードの実際のバイトを示します。
このコードについては以下に説明があります。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5

exception_table_lengthはexception_tableのエントリ数を格納します。

exception_tableは例外情報を表します。
exception_tableの各項目の内容は以下のようになります。
・start_pc、end_pc:例外ハンドラが有効になっているcode配列のインデックス値を表します。javaのコードでいうとtry区で囲まれている範囲になります。
・handler_pcの項目は例外ハンドラの開始するcode配列のインデックス値です。javaのコードでいうとcatch区で囲まれている範囲になります。
・catch_typeは0または、constant_poolテーブルへの有効なインデックスであり、そのインデックスは例外クラスをあらわすCONSTANT_Class_info構造体です。

attributes_count、attributes[]はこのクラスファイルで定義されているクラスにつての属性情報を表すattribute構造体の配列になっています。

HelloWorld.classのバイナリ解析

バイナリエディタの選定と設定

マシン語兄貴は、どんなバイナリエディタを使用しても16進数を読み解くことができるでしょうが、正直、令和の時代になって16進数を読むには辛いので、なるべく楽に読めそうなバイナリエディタを検討してみました。

今回は、BZEditorを使用します。
BZEditorの採用理由は以下の通りです。
・Windowsで使える(最近はビルドすればMacOSでも使える模様)
・構造体表示ができる。
・日ノ本言葉が使える
・ソースが公開されているので、その気になれば拡張できる。
 https://github.com/devil-tamachan/binaryeditorbz

その他のバイナリエディタを検討したい方はWikipediaにバイナリエディタの比較表を確認してみるといいでしょう。
https://en.wikipedia.org/wiki/Comparison_of_hex_editors
この中ではHxDが使いやすそうでした。

BZEditorの設定

構造体の定義

構造体の定義をBZEditorの実行ファイルと同じフォルダにあるBz.defで定義できます。
なお、固定サイズの構造体しか指定できませんので完璧に解析できません。

Bz.def

struct ClassFile_1 {
       BYTE magic[4];
       short minor_version;
       short majoir_version;
       short constant_pool_count;
} class;
struct ClassFile_2 {
       BYTE access_flags[2];
       short this_class;
       short super_class;
       short interfaces_count;
} class;

struct CONSTANT_Class {
       BYTE tag;
       short index;
} class;

struct CONSTANT_Methodref_info {
       BYTE tag;
       short class_index;
       short name_and_type_index;
} class;

struct CONSTANT_Fieldref {
       BYTE tag;
       short class_index;
       short name_and_type_index;
} class;

struct CONSTANT_NameAndType_info {
       BYTE tag;
       short name_index;
       short descriptor_index;
} class;

struct CONSTANT_String_info {
       BYTE tag;
       short string_index;
} class;

struct CONSTANT_Utf8 {
       BYTE tag;
       short length;
} class;

struct Code_attribute {
       short attribute_name_index;
       int attribute_length;
       short max_stack;
       short max_locals;
       int code_length;
} class;

このBZ.defはC言語ライクに記載できます。
使用できる型としては以下のコードのTYPESTR[NUM_MEMBERS] を参照してください。
https://github.com/devil-tamachan/binaryeditorbz/blob/master/Bz/BZFormVw.cpp

BZEditor起動後、「表示」>「構造体表示」をチェックすることで構造体表示用の子ウィンドウが表示されます。
image.png

アドレスをダブルクリックすることで、そのアドレスを始点とした構造体情報を表示します。
image.png

バイトの並びの変更

classファイルを解析するときはMotorolaを選択します。
image.png

BZEditorによるclassファイルの解析

では先頭からClassFileの解析をしていきます。

先頭~constant_pool_countまで

image.png

magicは「0xCAFEBABE」が表示されています。
minor versionは0
major_versionは52です。
constant_pool_countは26になり、次のバイトからconstant_poolのエントリになります。

constant_pool[]の解析

constant_pool[1]

image.png
1バイト目が0x0A=10なので、このconstant_poolのエントリはCONSTANT_Methodrefになります。
class_indexは6、name_and_type_indexは12です。
これらのインデックスが実際になにを指示しているかはconstant_poolを全て見終わった後に確認します。

constant_pool[2]

image.png
1バイト目が0x09なので、このconstant_poolのエントリはCONSTANT_Fieldrefになります。
class_indexは13、name_and_type_indexは14です。

constant_pool[3]

image.png

1バイト目が0x08なので、のconstant_poolのエントリはCONSTANT_Stringになります。
indexは15となります。

constant_pool[4]

image.png

1バイト目が0x0A=10なので、このconstant_poolのエントリはCONSTANT_Methodrefになります。
class_indexは16、name_and_type_indexは17です。

constant_pool[5]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは18になります。

constant_pool[6]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは19になります。

constant_pool[7]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは6となり、そのあとの6バイトで「\<init>」という文字を格納しています。

constant_pool[8]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは3となり、そのあとの3バイトで「()V」という文字を格納しています。

constant_pool[9]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは4となり、そのあとの4バイトで「Code」という文字を格納しています。

constant_pool[10]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは4となり、そのあとの4バイトで「main」という文字を格納しています。

constant_pool[11]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは22となり、そのあとの22バイトで「([Ljava/lang/String;)V」という文字を格納しています。

constant_pool[12]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは7,descriptor_indexは8になります。

constant_pool[13]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは20になります。

constant_pool[14]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは21,descriptor_indexは22になります。

constant_pool[15]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは11となり、そのあとの11バイトで「Hello World」という文字を格納しています。

constant_pool[16]

image.png

1バイト目が0x07なので、このconstant_poolのエントリはCONSTANT_Classになります。
indexは23になります。

constant_pool[17]

image.png

1バイトめが0x0C=12なので、このconstant_poolのエントリはCONSTANT_NameAndType_info構造体になります。
name_indexは24,descriptor_indexは25になります。

constant_pool[18]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは10となり、そのあとの10バイトで「HelloWorld」という文字を格納しています。

constant_pool[19]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは16となり、そのあとの16バイトで「java/lang/Object」という文字を格納しています。

constant_pool[20]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは16となり、そのあとの16バイトで「java/lang/System」という文字を格納しています。

constant_pool[21]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは3となり、そのあとの3バイトで「out」という文字を格納しています。

constant_pool[22]

image.png
1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは21となり、そのあとの21バイトで「Ljava/io/PrintStream;」という文字を格納しています。

constant_pool[23]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは19となり、そのあとの19バイトで「java/io/PrintStream」という文字を格納しています。

constant_pool[24]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは7となり、そのあとの7バイトで「println」という文字を格納しています。

constant_pool[25]

image.png

1バイト目が0x01なので、このconstant_poolのエントリはCONSTANT_Utf8になります。
lengthは21となり、そのあとの21バイトで「(Ljava/lang/String;)V」という文字を格納しています。

HelloWorld.classのconstant_poolのまとめ

constant_poolをまとめると以下のようになります。

No 構造体 内容
1 CONSTANT_Methodref class_indexは6:HelloWorld、name_and_type_indexは12: :V()
2 CONSTANT_Fieldref class_indexは13:java/lang/System、name_and_type_indexは14: out:Ljava/io/PrintStream;
3 CONSTANT_String indexは15:「Hello World」
4 CONSTANT_Methodref class_indexは16:java/io/PrintStream、name_and_type_indexは17:println:(Ljava/lang/String;)V
5 CONSTANT_Class indexは18:「HelloWorld」
6 CONSTANT_Class indexは19:「java/lang/Object」
7 CONSTANT_Utf8 「\<init>」という文字列です
8 CONSTANT_Utf8 「()V」という文字列です
9 CONSTANT_Utf8 「Code」という文字列です
10 CONSTANT_Utf8 「main」という文字列です
11 CONSTANT_Utf8 「([Ljava/lang/String;)V」という文字列です
12 CONSTANT_NameAndType_info name_indexは7:「\<init>」,descriptor_indexは8:「()V」。Method Descriptorsを参照
13 CONSTANT_Class indexは20;「java/lang/System」
14 CONSTANT_NameAndType_info name_indexは21:「out」,descriptor_indexは22:「Ljava/io/PrintStream;」Field Descriptorsを参照
15 CONSTANT_Utf8 「Hello World」という文字列です
16 CONSTANT_Class indexは23:「java/io/PrintStream」
17 CONSTANT_NameAndType_info name_indexは24:「println」,descriptor_indexは25:「(Ljava/lang/String;)V」Method Descriptorsを参照
18 CONSTANT_Utf8 「HelloWorld」という文字列です
19 CONSTANT_Utf8 「java/lang/Object」という文字列です
20 CONSTANT_Utf8 「java/lang/System」という文字列です
21 CONSTANT_Utf8 「out」という文字列です
22 CONSTANT_Utf8 「Ljava/io/PrintStream;」という文字列です
23 CONSTANT_Utf8 「java/io/PrintStream」という文字列です
24 CONSTANT_Utf8 「println」という文字列です
25 CONSTANT_Utf8 「(Ljava/lang/String;)V」という文字列です

access_flags~interfaces[]まで

image.png

・access_flagsは0x0021。つまりACC_SUPER(0x20) と ACC_PUBLIC(0x01)となります。

・this_classはconstant_pool[5]なのでHelloWorldクラスです。

・super_classはconstant_pool[6]なのでjava/lang/Objectクラスです。

・interfaces_countは0で、次に続くinterfaces[]は存在しません。

fields_count~fileds[]まで

image.png

fields_countが0のため、次に続くfiledsは存在しません。

methods_count~methods[]

image.png

methods_countは0x0002のため2件、method_info構造体が続きます。

method_info[0]

image.png

・access_flagsは0x0001。つまりACC_PUBLIC(0x01)となります。

・name_indexはconstant_pool[7]の「\<init>」となります。
 これはJavaコンパイル時に作成された暗黙のコンストラクタになります。

・description_indexはconstant_pool[8]の「()V」となります。

・attributes_countは1となり、attributes構造体が1つ存在します。

method_info[0].attributes[0]

image.png

attribute_name_indexはconstant_pool[9]の「Code」になるため、この構造体はCode_attribute構造体になります。

attribute_lengthは17バイトとなり、この構造体のサイズを決定します。

max_stackは1, max_localsは1となります。

code_lengthは5となり次の「0x 2A B7 00 01 B1」がバイトコードであることを表します。
0x2aはaload_0になります。この命令はthisをオペランドスタックにつみます。

0xb7はinvokesplecialになります。
この命令は後続の2バイトをconstant_poolのインデックスとしてメソッドを呼び出します。
今回の場合「0x00 01」なのでconstant_pool[1]である、「java/lang/Object."":()V」を呼び出すことになります。

0xb1はreturnになります。

exception_table_length, attributes_countはともに0になります。

method_info[1]

image.png

・access_flagsは0x0009。つまりACC_PUBLIC(0x01)とACC_STATIC(0x08)となります。

・name_indexはconstant_pool[10]の「main」となります。

・description_indexはconstant_pool[11]の「([Ljava/lang/String;)V」」となります。

・attributes_countは1となり、attributes構造体が1つ存在します。

method_info[1].attributes[0]

image.png

attribute_name_indexはconstant_pool[9]の「Code」になるため、この構造体はCode_attribute構造体になります。

attribute_lengthは21バイトとなり、この構造体のサイズを決定します。

max_stackは2, max_localsは1となります。

code_lengthは9となり次の「0x B2 00 02 12 03 B6 00 04 B1」がバイトコードであることを表します。

0xb2はgetstaticです。
この命令は後続の2バイトをconstant_poolのインデックスとしてstaticクラスからフィールドを取得します。
今回の場合「0x00 02」なのでconstant_pool[2]である、「java/lang/System」クラスの「 out:Ljava/io/PrintStream」を取得します。
取得した結果はオペランドスタックに積みます。

0x12はldcです。
この命令は後続の1バイトをconstant_poolのインデックスとして使用して、その内容をオペランドスタックに積みます。
今回の場合は「0x03」なのでconstant_pool[3]の「Hello World」という文字列をオペランドスタックに積みます。

0xb6はinvokevirtualです。
この命令は後続の2バイトをconstant_poolのインデックスとして使用して、そのメソッドを実行します。
今回の場合は「0x00 04」なのでconstant_pool[4]である、java/io/PrintStreamクラスのprintln:(Ljava/lang/String;)Vを実行します。

0xb1はreturnになります。

exception_table_length, attributes_countはともに0になります。

ClassFile のattributes_count、attributes[]

image.png

attributes_countが0なのでattributesのデータは存在しません。

クラスファイルの解析のまとめ

このようにJVMの仕様書とバイナリエディタでクラスファイルの解析が行えます。
ただし、こんな面倒なバイナリエディタを使用しなくてもjavapコマンドで解析できます。

>javap -v HelloWorld
Classfile /C:/XXXXXXX/HelloWorld.class
  Last modified 2019/06/09; size 340 bytes
  MD5 checksum 3ee6d0a4b44197baaeb0cec79a0b73d3
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#12         // java/lang/Object."<init>":()V
   #2 = Fieldref           #13.#14        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #15            // Hello World
   #4 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #18            // HelloWorld
   #6 = Class              #19            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = NameAndType        #7:#8          // "<init>":()V
  #13 = Class              #20            // java/lang/System
  #14 = NameAndType        #21:#22        // out:Ljava/io/PrintStream;
  #15 = Utf8               Hello World
  #16 = Class              #23            // java/io/PrintStream
  #17 = NameAndType        #24:#25        // println:(Ljava/lang/String;)V
  #18 = Utf8               HelloWorld
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/System
  #21 = Utf8               out
  #22 = Utf8               Ljava/io/PrintStream;
  #23 = Utf8               java/io/PrintStream
  #24 = Utf8               println
  #25 = Utf8               (Ljava/lang/String;)V
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

バイナリを読むより全然らくですね。
いやぁ、これでHelloWorldの理解が深まりました。めでたしめでたし。

(調査を)まだやるかい

もうちょっとこのHelloWorld.classがどう動くか掘り下げてみようと思います。

インタラプタとコンパイラ

よくJavaは複数回コンパイルされる場合があるといわれますが、どういうことでしょうか。
これについてはTobias Hartmann氏が記述したThe Java HotSpot VMに記載があります。

image.png

上記の図のように作成されたバイトコードはC1またはC2でコンパイルされたマシンコードで実行されるか、インタプリタで実行されるかのいずれかになります。
また、Java8ではインタプリタで動作していたものが途中からC1でコンパイルしたものに変わったり、インタプリタ→C1→C2と段階的にコンパイルしていく場合があります。

つまり、私はHelloWorldがどう動いたなんて全然わかっちゃいなかったんだ・・・

コンパイルされたのかどうか

実行されたバイトコードがインタプリタで実行されたのか、それともコンパイルされたマシンコードで実行されたかを確認する方法はあるのでしょうか?
javaを実行時に「-XX:+PrintCompilation」を使用することで、それは判明します。

>java  -XX:+PrintCompilation HelloWorld
     73    1       3       java.lang.String::hashCode (55 bytes)
     74    2       3       java.lang.String::equals (81 bytes)
     75    4     n 0       java.lang.System::arraycopy (native)   (static)
     76    3       4       java.lang.String::charAt (29 bytes)
     76    5       3       java.lang.Object::<init> (1 bytes)
     78    6       4       sun.misc.ASCIICaseInsensitiveComparator::toLower (16 bytes)
     78    7       4       sun.misc.ASCIICaseInsensitiveComparator::isUpper (18 bytes)
     79    8       4       java.lang.String::length (6 bytes)
     79    9       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
     80   10       3       java.lang.Character::toLowerCase (9 bytes)
     80   11       3       java.lang.CharacterData::of (120 bytes)
     81   15       1       java.lang.Object::<init> (1 bytes)
     81    5       3       java.lang.Object::<init> (1 bytes)   made not entrant
     81   12       3       java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
     82   13       3       java.lang.CharacterDataLatin1::getProperties (11 bytes)
     82   17       3       java.io.WinNTFileSystem::isSlash (18 bytes)
     84   16       3       java.lang.AbstractStringBuilder::append (29 bytes)
     84   18  s    3       java.lang.StringBuffer::append (13 bytes)
     85   14       3       java.lang.Math::min (11 bytes)
     86   19       3       java.lang.StringBuilder::append (8 bytes)
     88   20       3       java.lang.String::getChars (62 bytes)
     90   22       3       java.lang.String::indexOf (70 bytes)
     91   21       3       java.util.Arrays::copyOfRange (63 bytes)
     92   23       3       java.lang.System::getSecurityManager (4 bytes)
Hello World

PrintCompilation を使用することでコンパイルされて実行されたメソッドがわかります。
この出力の詳細についてはStackOverflowとかにありますが、せっかくなのでOpenSDKのソースコードを見てみることにします。

Java8のソースコードは以下から取得可能です。
https://download.java.net/openjdk/jdk8u40/ri/openjdk-8u40-src-b25-10_feb_2015.zip

このコード中でPrintCompilationを付与した際に出力される内容を作成していると考えられる下記のコードをみてみます。

openjdk\hotspot\src\share\vm\compiler\compileBroker.cpp

// ------------------------------------------------------------------
// CompileTask::print_compilation_impl
void CompileTask::print_compilation_impl(outputStream* st, Method* method, int compile_id, int comp_level,
                                         bool is_osr_method, int osr_bci, bool is_blocking,
                                         const char* msg, bool short_form) {
  if (!short_form) {
    st->print("%7d ", (int) st->time_stamp().milliseconds());  // print timestamp
  }
  st->print("%4d ", compile_id);    // print compilation number

  // For unloaded methods the transition to zombie occurs after the
  // method is cleared so it's impossible to report accurate
  // information for that case.
  bool is_synchronized = false;
  bool has_exception_handler = false;
  bool is_native = false;
  if (method != NULL) {
    is_synchronized       = method->is_synchronized();
    has_exception_handler = method->has_exception_handler();
    is_native             = method->is_native();
  }
  // method attributes
  const char compile_type   = is_osr_method                   ? '%' : ' ';
  const char sync_char      = is_synchronized                 ? 's' : ' ';
  const char exception_char = has_exception_handler           ? '!' : ' ';
  const char blocking_char  = is_blocking                     ? 'b' : ' ';
  const char native_char    = is_native                       ? 'n' : ' ';

  // print method attributes
  st->print("%c%c%c%c%c ", compile_type, sync_char, exception_char, blocking_char, native_char);

  if (TieredCompilation) {
    if (comp_level != -1)  st->print("%d ", comp_level);
    else                   st->print("- ");
  }
  st->print("     ");  // more indent

  if (method == NULL) {
    st->print("(method)");
  } else {
    method->print_short_name(st);
    if (is_osr_method) {
      st->print(" @ %d", osr_bci);
    }
    if (method->is_native())
      st->print(" (native)");
    else
      st->print(" (%d bytes)", method->code_size());
  }

  if (msg != NULL) {
    st->print("   %s", msg);
  }
  if (!short_form) {
    st->cr();
  }
}

1列目はタイプスタンプが出力されます。

2列目はcompilation_idとmethod_attributesです。cocmpilation_idは4桁の数字です。
method_attributesはフラグの組み合わせで以下のように表示されます。

文字 条件
% OCRメソッドの場合.enum型のMethodCompilationが定義されていてInvocationEntryBciとInvalidOSREntryBciがあって、このInvalidOSREntryBciの場合
s synchronizedの場合
! exception_handlerを持つ場合
b blockingの場合
n nativeコードの場合

3列目はTieredCompilationがONの場合、コンパイルレベルが表示されます。
このTieredCompilationについては-XX:-TieredCompilationまたは+XX:-TieredCompilationオプションで制御できますが、Java8の場合デフォルトはONです。
コンパイルレベルは以下のようになります。

level 内容
0 interpreter
1 C1 with full optimization (no profiling)
2 C1 with limited profiling
3 C1 with full profiling
4 C2

つまり、同じC1といっても3段階に分かれています。

4列目はメソッド名が出力されます。

さて最初の-XX:+PrintCompilationの出力結果を見てみましょう。
そこにはHelloWorldクラスのmainは含まれいないので、そこのコードはインタプリタで実行されていることがわかります。

マシンコードにコンパイルされているとかいっても、その内容はみえないの?

C1,C2でコンパイルして作成されたマシンコードはファイルに出力されるわけでもなくメモリ上に存在するだけです。
この内容を確認するにはいくつかの手順が必要です。

まず、逆アセンブラができるhsdis-amd64.dllを入手します。
Windowsの場合は以下からダウンロードできるでしょう。
https://sourceforge.net/projects/fcml/files/

DLLをダウンロードしたら、そのDLLにパスを通してください。

その後、以下のコマンドを実行します。

java -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly -XX:+LogCompilation  HelloWorld

カレントディレクトリにlogファイルが作成されて、どんなマシンコードを作ったか閲覧できます。

出力例

Decoding compiled method 0x0000000002d00750:
Code:
RIP: 0x2d008a0 Code size: 0x000001f0
[Entry Point]
[Constants]
  # {method} {0x00000000192f4fc0} &apos;hashCode&apos; &apos;()I&apos; in &apos;java/lang/String&apos;
  #           [sp+0x40]  (sp of caller)
  0x0000000002d008a0: mov     r10d,dword ptr [rdx+8h]
  0x0000000002d008a4: shl     r10,3h
  0x0000000002d008a8: cmp     r10,rax
  0x0000000002d008ab: jne     2c35f60h          ;   {runtime_call}
  0x0000000002d008b1: nop     word ptr [rax+rax+0h]
  0x0000000002d008bc: nop
[Verified Entry Point]
  0x0000000002d008c0: mov     dword ptr [rsp+0ffffffffffffa000h],eax
  0x0000000002d008c7: push    rbp
  0x0000000002d008c8: sub     rsp,30h
  0x0000000002d008cc: mov     rax,193e7ac8h
  0x0000000002d008d6: mov     esi,dword ptr [rax+8h]
  0x0000000002d008d9: add     esi,8h
  0x0000000002d008dc: mov     dword ptr [rax+8h],esi
// 略

ログファイルが大量でみずらい・・・

マシンコードを出力したログファイルは大量に情報が出力されていて、目的の情報をみつけるのにも苦労するでしょう。
この場合、JitWatchで閲覧するとよいでしょう。
https://github.com/AdoptOpenJDK/jitwatch/

image.png

image.png

詳しい使い方は以下を参照してください。

JITWatchでJITコンパイルを見よう!
https://www.sakatakoichi.com/entry/2014/12/04/202747

インタプリタは、どうやってバイトコードを解釈しているのか?

いままでで、HelloWorld::mainはインタプリタで動作していることがわかりました。
では以下のgetstatic~returnといった命令は、具体的にどこで、どうやって処理されているのでしょうか?

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

この謎を解明しようとOpenSDKのソースコードを漁っていた調査班はついに、その該当箇所を発見しました。
getstaticや、ldcなどの命令は解釈と実行はbytecodeInterpreter.cppのrunWithChecks()/run()で行っているのを発見したのです。

/*
 * BytecodeInterpreter::run(interpreterState istate)
 * BytecodeInterpreter::runWithChecks(interpreterState istate)
 *
 * The real deal. This is where byte codes actually get interpreted.
 * Basically it's a big while loop that iterates until we return from
 * the method passed in.
 *
 * The runWithChecks is used if JVMTI is enabled.
 *
 */
# if defined(VM_JVMTI)
void
BytecodeInterpreter::runWithChecks(interpreterState istate) {
# else
void
BytecodeInterpreter::run(interpreterState istate) {
# endif
  // 略
# ifndef USELABELS
  while (1)
# endif
  {
# ifndef PREFETCH_OPCCODE
      opcode = *pc;
# endif
      // Seems like this happens twice per opcode. At worst this is only
      // need at entry to the loop.
      // DEBUGGER_SINGLE_STEP_NOTIFY();
      /* Using this labels avoids double breakpoints when quickening and
       * when returing from transition frames.
       */
  opcode_switch:
      assert(istate == orig, "Corrupted istate");
      /* QQQ Hmm this has knowledge of direction, ought to be a stack method */
      assert(topOfStack >= istate->stack_limit(), "Stack overrun");
      assert(topOfStack < istate->stack_base(), "Stack underrun");

# ifdef USELABELS
      DISPATCH(opcode);
# else
      switch (opcode)
# endif
      {
      CASE(_nop):
          UPDATE_PC_AND_CONTINUE(1);
      // 略
      }
    }
  }
}

これはメソッド中の全てのバイトコードを実行するまでwhileでループし、命令コードに合わせてCASE区で分岐されて実行されています。
たとえばgetstaticは以下のような実装になっています。

getstatic

      CASE(_getfield):
      CASE(_getstatic):
        {
          u2 index;
          ConstantPoolCacheEntry* cache;
          // 注釈:現在のバイトコードの位置pc+1から2バイトデータを取得してindexに格納する
          index = Bytes::get_native_u2(pc+1);

          // QQQ Need to make this as inlined as possible. Probably need to
          // split all the bytecode cases out so c++ compiler has a chance
          // for constant prop to fold everything possible away.
          // 注釈:constatnt_tableからindexを指定して値をとる。
          cache = cp->entry_at(index);
          if (!cache->is_resolved((Bytecodes::Code)opcode)) {
            CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
                    handle_exception);
            cache = cp->entry_at(index);
          }

# ifdef VM_JVMTI
          if (_jvmti_interp_events) {
            int *count_addr;
            oop obj;
            // Check to see if a field modification watch has been set
            // before we take the time to call into the VM.
            count_addr = (int *)JvmtiExport::get_field_access_count_addr();
            if ( *count_addr > 0 ) {
              if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
                obj = (oop)NULL;
              } else {
                obj = (oop) STACK_OBJECT(-1);
                VERIFY_OOP(obj);
              }
              CALL_VM(InterpreterRuntime::post_field_access(THREAD,
                                          obj,
                                          cache),
                                          handle_exception);
            }
          }
# endif /* VM_JVMTI */

          oop obj;
          if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
            // 注釈:constant_table[2]のクラス情報を取得してobjに入れる
            Klass* k = cache->f1_as_klass();
            obj = k->java_mirror();
            MORE_STACK(1);  // Assume single slot push
          } else {
            obj = (oop) STACK_OBJECT(-1);
            CHECK_NULL(obj);
          }

          //
          // Now store the result on the stack
          //
          TosState tos_type = cache->flag_state();
          // 注釈:constant_table[2]のフィールド情報を取得してfield_offsetに入れる
          int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
              OrderAccess::fence();
            }
            if (tos_type == atos) {
              VERIFY_OOP(obj->obj_field_acquire(field_offset));
              SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
              MORE_STACK(1);
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field_acquire(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field_acquire(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field_acquire(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field_acquire(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
              MORE_STACK(1);
            }
          } else {
            if (tos_type == atos) {
              // 注釈:constant_table[2]のクラスのフィールドを取得してその結果をオブジェクトとしてスタックに格納する。
              VERIFY_OOP(obj->obj_field(field_offset));
              SET_STACK_OBJECT(obj->obj_field(field_offset), -1);
            } else if (tos_type == itos) {
              SET_STACK_INT(obj->int_field(field_offset), -1);
            } else if (tos_type == ltos) {
              SET_STACK_LONG(obj->long_field(field_offset), 0);
              MORE_STACK(1);
            } else if (tos_type == btos) {
              SET_STACK_INT(obj->byte_field(field_offset), -1);
            } else if (tos_type == ctos) {
              SET_STACK_INT(obj->char_field(field_offset), -1);
            } else if (tos_type == stos) {
              SET_STACK_INT(obj->short_field(field_offset), -1);
            } else if (tos_type == ftos) {
              SET_STACK_FLOAT(obj->float_field(field_offset), -1);
            } else {
              SET_STACK_DOUBLE(obj->double_field(field_offset), 0);
              MORE_STACK(1);
            }
          }
          // getstaticの3バイト先の命令を実行する。
          UPDATE_PC_AND_CONTINUE(3);
         }
// Have to do this dispatch this way in C++ because otherwise gcc complains about crossing an
// initialization (which is is the initialization of the table pointer...)
# define DISPATCH(opcode) goto *(void*)dispatch_table[opcode]
// 略
# define UPDATE_PC_AND_CONTINUE(opsize) {                        \
        pc += opsize; opcode = *pc;                             \
        DO_UPDATE_INSTRUCTION_COUNT(opcode);                    \
        DEBUGGER_SINGLE_STEP_NOTIFY();                          \
        DISPATCH(opcode);                                       \
    }

次の命令を実行する場合はDISPACHが実行されます。
これはgoto文になっており、命令用のラベルにジャンプすることで次の命令を実行しています。

このようにインタラプタに関しては、このコードを起点に見ていけばなんとなくどんな処理をしているかの雰囲気はつかめそうです。
この時点で3万文字こえているのでこれ以上解析した情報をまとめて乗せるのはつらいお

まとめ

今回は初心に戻ってHelloWorldがどのように動作するか見てみました。

「百聞は一見にしかず 百見は一触にしかず」とは言いますが、すみません、HelloWorldなめてました。
すごくめんどくさかったです。

参考:

Demystifying the JVM: Interpretation, JIT and AOT Compilation
https://metebalci.com/blog/demystifying-the-jvm-interpretation-jit-and-aot-compilation/#disqus_thread

DEMYSTIFYING THE JVM: JVM VARIANTS, CPPINTERPRETER AND TEMPLATEINTERPRETER
https://metebalci.com/blog/demystifying-the-jvm-jvm-variants-cppinterpreter-and-templateinterpreter/#disqus_thread

JITWatchによるJava JITコンパイルの調査
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA15-Architect-newland.pdf

JITWatchでJITコンパイルを見よう!
https://www.sakatakoichi.com/entry/2014/12/04/202747

[Java]〈Hello World〉をバイナリエディタだけで使って出力させてみた
https://tech.recruit-mp.co.jp/etc/java_class_hello_world/

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です