読者です 読者をやめる 読者になる 読者になる

JVM仕様にあるクラスファイルのフォーマットについて

これはJVMアドベントカレンダー1日目の記事です。


JVM Advent Calendar 2014 - Qiita


JVM仕様にあるクラスファイルのフォーマットについて書きます。

JavaのクラスファイルのフォーマットはJVM仕様の4章で定義されています。

Chapter 4. The class File Format

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


全く分からない状態からこれらのフォーマットについてひとつづつ読んでいくのは辛いと思うのでざっくり説明します。

クラスファイルが必要なので適当なJavaソースコードを作ってコンパイルします。

class AdventCalendar2014 {
    public static void main(String[] args) {
        System.out.println("Hello JVM Advent Calendar 2014!");
    }
}
% javac AdventCalendar2014.java
% ls
AdventCalendar2014.class AdventCalendar2014.java


クラスファイルが出来たので今度はバイナリを覗いてみましょう。
以前そういうやつ書いたことがあるのでこれを使うことにします。


ClassReader

これをそのままコピってきてコンパイルしてさっきのクラスファイルを引数に指定すると、
2進数と16進数でクラスファイルの中身を出力します。

% java ClassReader /path/to/AdventCalendar2014.class
Binary  |Hex
------------
11001010 CA
11111110 FE
10111010 BA
10111110 BE
00000000 0
00000000 0
00000000 0
00110100 34
00000000 0
00011101 1D
00001010 A
00000000 0
00000110 6
00000000 0
00001111 F
00001001 9
00000000 0
00010000 10
00000000 0
00010001 11
00001000 8
00000000 0
00010010 12
00001010 A
00000000 0
00010011 13
00000000 0
00010100 14
00000111 7
00000000 0
00010101 15
00000111 7
00000000 0
00010110 16
00000001 1
00000000 0
00000110 6
00111100 3C
01101001 69
01101110 6E
01101001 69
01110100 74
00111110 3E
・・・

2進数は読みにくいので16進数だけにしたら以下のような感じになりました。

CAFEBABE00000034001D0A0006000F09001000110800120A001300140700150700160100063C696E
69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100046D6169
6E010016285B4C6A6176612F6C616E672F537472696E673B295601000A536F7572636546696C6501
0017416476656E7443616C656E646172323031342E6A6176610C000700080700170C001800190100
1F48656C6C6F204A564D20416476656E742043616C656E64617220323031342107001A0C001B001C
010012416476656E7443616C656E646172323031340100106A6176612F6C616E672F4F626A656374
0100106A6176612F6C616E672F53797374656D0100036F75740100154C6A6176612F696F2F507269
6E7453747265616D3B0100136A6176612F696F2F5072696E7453747265616D0100077072696E746C
6E010015284C6A6176612F6C616E672F537472696E673B2956002000050006000000000002000000
070008000100090000001D00010001000000052AB70001B100000001000A00000006000100000002
0009000B000C00010009000000250002000100000009B200021203B60004B100000001000A000000
0A000200000005000800060001000D00000002000E


これを最初のJVM仕様のクラスフォーマットと照らし合わせていきましょう。
magic っていうのはマジックナンバーJavaクラスの先頭はこの値が入ります。
これは Cafe(C++, A Front End)というC++コンパイラのプロダクトマネージャの Kim Polese さんの愛称(babe)からきているというのがまことしやかに噂として流れています。

CA FE BA BE // u4 magic;
00 00       // u2 minor_version;
00 34       // u2 major_version;
00 1D       // u2 constant_pool_count;

ってみていくと cp_info っていうよく分からないのが登場しました。
これもJVM仕様読んでいくと 4.4 で以下のように定義されています。

cp_info {
    u1 tag;
    u1 info[];
}

一番最初の tag でデータの種類を定義していて、その値によって次の info[] の読み方が変わります。
例えば tag = 7 の場合は CONSTANT_Class なので、その場合は CONSTANT_Class_info を読みます。
というような感じで進めていくと以下のようになります。

CA FE BA BE     // u4 magic
00 00           // u2 minor_version = 0
00 34           // u2 major_version = 57
00 1D           // u2 constant_pool_count = 29
// cp_info #1
0A              // tag = 10 (CONSTANT_Methodref)
00 06           // u2 class_index = 6
00 0F           // u2 name_and_type_index = 15
// cp_info #2
09              // tag = 9 (CONSTANT_Fieldref)
00 10           // u2 class_index = #16
00 11           // u2 name_and_type_index = #17
// cp_info #3
08              // tag = 8 (CONSTANT_String)
00 12           // u2 string_index = #18
// cp_info #4
0A              // tag = 10 (CONSTANT_Methodref)
00 13           // u2 class_index = #19
00 14           // u2 name_and_type_index = #20
// cp_info #5
07              // tag = 7 (CONSTANT_Class)
00 15           // u2 name_index = #21
// cp_info #6
07              // tag = 7 (CONSTANT_Class)
00 16           // u2 name_index = #18
// cp_info #7
01              // u1 tag = 1 (CONSTANT_Utf8)
00 06           // u2 length = 6
3C 69 6E 69 74  // u1 bytes[length] = <init>
3E
// cp_info #8
01              // u1 tag = 1 (CONSTANT_Utf8)
00 03           // u2 length = 3
28 29 56        // u1 bytes[length] = ()V
// cp_info #9
01              // u1 tag = 1 (CONSTANT_Utf8)
00 04           // u2 length = 4
43 6F 64 65     // u1 bytes[length] = Code
// cp_info #10
01              // u1 tag = 1 (CONSTANT_Utf8)
00 0F           // u2 length = 15
4C 69 6E 65 4E  // u1 bytes[length] = LineNumberTable
75 6D 62 65 72 
54 61 62 6C 65 
// cp_info #11
01              // u1 tag = 1 (CONSTANT_Utf8)
00 04           // u2 length = 4
6D 61 69 6E     // u1 bytes[length] = main
// cp_info #12
01              // u1 tag = 1 (CONSTANT_Utf8)
00 16           // u2 length = 22
28 5B 4C 6A 61  // u1 bytes[length] = ([Ljava/lang/String;)V
76 61 2F 6C 61
6E 67 2F 53 74
72 69 6E 67 3B
29 56 
// cp_info #13
01              // u1 tag = 1 (CONSTANT_Utf8)
00 0A           // u2 length = 10
53 6F 75 72 63  // u1 bytes[length] = SourceFile
65 46 69 6C 65 
// cp_info #14
01              // u1 tag = 1 (CONSTANT_Utf8)
00 17           // u2 length = 23
41 64 76 65 6E  // u1 bytes[length] = AdventCalendar2014.java
74 43 61 6C 65 
6E 64 61 72 32 
30 31 34 2E 6A 
61 76 61 
// cp_info #15
0C              // u1 tag = 12 (CONSTANT_NameAndType)
00 07           // u2 name_index = #7
00 08           // u2 descriptor_index = #8
// cp_info #16
07              // tag = 7 (CONSTANT_Class)
00 17           // u2 name_index = #23
0C              // u1 tag = 12 (CONSTANT_NameAndType)
00 18           // u2 name_index = #24
00 19           // u2 descriptor_index = #25
・・・


まぁあとは同じです。。
なんと驚くべきことに、ここまですべて手作業で解析してきましたが、さすがにもうしんどいです。

Javaチョットデキル人はご存知の人も多いですが、この解析は javap すれば簡単に出力してくれます。

% javap -c -verbose AdventCalendar2014
Classfile /path/to/AdventCalendar2014.class
  Last modified 2014/11/30; size 461 bytes
  MD5 checksum 94ccbcbc2ad1fdedf9c05cf088ca89f5
  Compiled from "AdventCalendar2014.java"
class AdventCalendar2014
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello JVM Advent Calendar 2014!
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // AdventCalendar2014
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               AdventCalendar2014.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello JVM Advent Calendar 2014!
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               AdventCalendar2014
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  AdventCalendar2014();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  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 JVM Advent Calendar 2014!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "AdventCalendar2014.java"


こんな感じです。

明日は @sugarlife さんの「OpenJDKのソースコード配置(1-2サブディレクトリ程度まで)」です!
よろしくお願いします!