JavaクラスファイルのCONSTANT_Long_infoがコンスタントプールのエントリを2つ使う件

JavaのコンスタントプールのLong_info構造体について、わからんーとTwitterで言っていたところ、finalfusionさんに教えてもらって疑問が解けたので、メモしておきます。

クラスファイルのおさらい

Javaクラスファイルのバイナリの仕様は、次のようになっています。

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

constant_pool配列には、その上の (constant_pool_countの数−1) 個の要素があります。仕様書ではエントリと呼ばれています。

constant_pool配列の要素

constant_pool配列の各要素は、内部にそれぞれ1バイトのtagを持っています。

各要素が、tag以外に、どれくらいのサイズのどんな情報を格納するかは、tagによって示される「要素の種類」(構造体の種類)ごとに異なります。

たとえば、tagが「7」であれば、その配列要素はCONSTANT_Class_info構造体であり、1バイトのtagと、2バイトのname_indexを持つことになります。

その定義は、仕様でこんなふうに書かれています。

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

tagの種類は、現時点で14種類のようですね。

# このあたりの詳しい読み方は、バイナリ勉強会で教えてもらいました。ご存じない方は、よろしければ、まとめをどうぞ。

(疑問 その1)high_bytesとlow_bytesがある意味

分からなかったのは、CONSTANT_Long_infoについてでした。

CONSTANT_Long_infoの仕様は、次のようになっています。

CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

そもそもの疑問は、high_bytesとlow_bytesに(以下の式で得られた)"the long constant bits"が格納されるという文章を読んで、high_bytesとlow_bytesの2つの項目があるのに、両方に同じ値を入れるの?と思ったことでした。

The high_bytes and low_bytes items are converted into the long constant bits, which is equal to

((long) high_bytes << 32) + low_bytes

http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.5

これについては、「8バイトのlong値を一度に表現できないから、上位4バイトと下位4バイトに分けて表現する」と教えていただきました。

上位4バイトのhigh_bytesと、下位4バイトのlow_bytesを合わせて、CONSTANT_Long_info構造体1つで、1つのlong値を表現するんですね。

(疑問 その2)2エントリを使う、とはどういう意味か

上の話には聞き覚えがあるので、以前どこかで聴いたのに、忘れてしまったのに違いありません。

確かに仕様にもそう書いてあります。……あります?

All 8-byte constants take up two entries in the constant_pool table of the class file. If a CONSTANT_Long_info or CONSTANT_Double_info structure is the item in the constant_pool table at index n, then the next usable item in the pool is located at index n+2.

http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.5

この「take up two entries」の意味が分からない、というのが2つ目の疑問でした。

最初は、Longを表現するために、constant_pool配列の要素であるCONSTANT_Long_info構造体を、2つ必要とするのかと思いました。しかし、8バイトのlong値もhigh_bytwsとlow_bytesで表現できてしまうのですから、2つ並べるには及びません。

というところで、finalfusionさんから、次のページを教えていただいて、疑問が解けました。

Longを表現するのに必要なバイナリは、あくまでもCONSTANT_Long_info構造体1つ分です。しかし、constant_pool配列要素のカウンタを2つ分消費するという意味だったんですね。

勉強になりました。ありがとうございました。

(おまけ)CONSTANT_Long_infoで2つのカウンタが消費されることを確認する

longを使ったJavaのコードを用意し、コンパイルします。

public class PreIncrement2 {
	public long increment() {
		long n = 10000000L;
		++n;
		return n;
	}
}

(ひどすぎる、というなかれ。下手に複雑にすると目視で読むのに一苦労です)(それでもクラス名がひどいか…)

これをバイナリエディタのBZで開いて、ダンプファイルに落としたものが、次のとおりです。

        +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F  0123456789ABCDEF
000000  CA FE BA BE 00 00 00 33-00 11 0A 00 05 00 0E 05  .......3........ 
000010  00 00 00 00 00 98 96 80-07 00 0F 07 00 10 01 00  ................ 
000020  06 3C 69 6E 69 74 3E 01-00 03 28 29 56 01 00 04  .<init>...()V... 
000030  43 6F 64 65 01 00 0F 4C-69 6E 65 4E 75 6D 62 65  Code...LineNumbe 
000040  72 54 61 62 6C 65 01 00-09 69 6E 63 72 65 6D 65  rTable...increme 
000050  6E 74 01 00 03 28 29 4A-01 00 0A 53 6F 75 72 63  nt...()J...Sourc 
000060  65 46 69 6C 65 01 00 12-50 72 65 49 6E 63 72 65  eFile...PreIncre 
000070  6D 65 6E 74 32 2E 6A 61-76 61 0C 00 06 00 07 01  ment2.java...... 
000080  00 0D 50 72 65 49 6E 63-72 65 6D 65 6E 74 32 01  ..PreIncrement2. 
000090  00 10 6A 61 76 61 2F 6C-61 6E 67 2F 4F 62 6A 65  ..java/lang/Obje 
0000A0  63 74 00 21 00 04 00 05-00 00 00 00 00 02 00 01  ct.!............ 
0000B0  00 06 00 07 00 01 00 08-00 00 00 1D 00 01 00 01  ................ 
0000C0  00 00 00 05 2A B7 00 01-B1 00 00 00 01 00 09 00  ....*........... 
0000D0  00 00 06 00 01 00 00 00-01 00 01 00 0A 00 0B 00  ................ 
0000E0  01 00 08 00 00 00 2A 00-04 00 03 00 00 00 0A 14  ......*......... 
0000F0  00 02 40 1F 0A 61 40 1F-AD 00 00 00 01 00 09 00  ..@..a@......... 
000100  00 00 0E 00 03 00 00 00-03 00 04 00 04 00 08 00  ................ 
000110  05 00 01 00 0C 00 00 00-02 00 0D                 ...........      


最初は、次のように間違った読み方をしていました。

※注意※ 次の読み方は誤りです。正しい読み方は、この記事の後ろの方にあります。

u4 magic               CAFEBABE
u2 minor_version       00 00
u2 major_version       00 33
u2 constant_pool_count 00 11  # (0x11==17) - 1個


#1  u1 tag                   0A # CONSTANT_Methodref_info
    u2 class_index           00 05
    u2 name_and_type_ index  00 0E

#2  u1 tag                   05 # CONSTANT_Long_info
    u4 high_bytes            00 00 00 00
    u4 low_bytes             00 98 96 80

# ★この次のカウントが誤り。Longの後なので、本当は4にしなければならない。
#3  u1 tag                   07 # CONSTANT_Class_info
    u2 name_index            00 0F

#4  u1 tag                   07 # CONSTANT_Class_info
    u2 name_index            00 10

#5  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 06
    u1 bytes[length]         3C 69 6E 69 74 3E # <init>

#6  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 03
    u1 bytes[length]         28 29 56 # ()V

#7  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 04
    u1 bytes[length]         43 6F 64 65 # Code

#8  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 0F
    u1 bytes[length]         4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65

#9  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 09
    u1 bytes[length]         69 6E 63 72 65 6D 65 6E 74

#10 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 03
    u1 bytes[length]         28 29 4A

#11 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 0A
    u1 bytes[length]         53 6F 75 72 63 65 46 69 6C 65

#12 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 12
    u1 bytes[length]         50 72 65 49 6E 63 72 65 6D 65 6E 74 32 2E 6A 61 76 61

#13 u1 tag                   0C # CONSTANT_NameAndType_info
    u2 name_index            00 06
    u2 descriptor_index      00 07

(以下略)

これで何がまずいかというと、constant_pool配列の13番目の要素(と上では示されているモノ)であるCONSTANT_NameAndType_info構造体のname_indexとdescriptor_indexが、明らかに正しくない点です。

CONSTANT_NameAndType_infoのname_index項目は、いわゆるJavaの識別子を格納したconstant_poolのエントリを参照していなければなりません。「」が入ったエントリでもOKです。

name_index
The value of the name_index item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure representing either the special method name (§2.9) or a valid unqualified name (§4.2.2) denoting a field or method.
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.6

しかし、上のように誤って順序づけられたダンプを見ると、name_indexが示す6番目のエントリは、文字列「()V」です。これはフィールド名でもメソッド名でもありません。

さらに、CONSTANT_NameAndType_infoのdescriptor_index項目は、フィールドディスクリプタやメソッドディスクリプタでなければなりません。詳細は割愛しますが、これにも違反しています。

で、おかしいなーーと悩んでいた時、ちょうど上のページを教えてもらい、間違いに気づきました :)

正しくは、こう読むんですね。

(正しい読み方バージョン)

u4 magic               CAFEBABE
u2 minor_version       00 00
u2 major_version       00 33
u2 constant_pool_count 00 11  # (0x11==17) - 1個


#1  u1 tag                   0A # CONSTANT_Methodref_info
    u2 class_index           00 05
    u2 name_and_type_ index  00 0E

#2  u1 tag                   05 # CONSTANT_Long_info
    u4 high_bytes            00 00 00 00
    u4 low_bytes             00 98 96 80

# ☆Longの後なので、カウンタを1つ進める。
#4  u1 tag                   07 # CONSTANT_Class_info
    u2 name_index            00 0F

#5  u1 tag                   07 # CONSTANT_Class_info
    u2 name_index            00 10

#6  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 06
    u1 bytes[length]         3C 69 6E 69 74 3E # <init>

#7  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 03
    u1 bytes[length]         28 29 56 # ()V

#8  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 04
    u1 bytes[length]         43 6F 64 65 # Code

#9  u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 0F
    u1 bytes[length]         4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65

#10 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 09
    u1 bytes[length]         69 6E 63 72 65 6D 65 6E 74

#11 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 03
    u1 bytes[length]         28 29 4A

#12 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 0A
    u1 bytes[length]         53 6F 75 72 63 65 46 69 6C 65

#13 u1 tag                   01 # CONSTANT_Utf8_info
    u2 length                00 12
    u1 bytes[length]         50 72 65 49 6E 63 72 65 6D 65 6E 74 32 2E 6A 61 76 61

#14 u1 tag                   0C # CONSTANT_NameAndType_info
    u2 name_index            00 06
    u2 descriptor_index      00 07

(以下略)

2番目のエントリがCONSTANT_Long_infoなので、3番目のエントリが使用不能になり、次のエントリは4番目になります。

この場合、name_indexが示す先の「正しい」6番目のエントリは、「」となり、無事仕様どおりであることが確認できました。

descriptor_indexが示す先の7番目のエントリも、「V()」ですから、メソッドディスクリプタです。

javapの出力結果

javapの出力を注意深く見ていれば、上のようなことはすぐ分かりそうなものですが、まったく気がつきませんでした^^;;

>javap -v PreIncrement2.class
# (略)
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         //  java/lang/Object."<init>":()V
   #2 = Long               10000000l
   #4 = Class              #15            //  PreIncrement2
   #5 = Class              #16            //  java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               increment
  #11 = Utf8               ()J
  #12 = Utf8               SourceFile
  #13 = Utf8               PreIncrement2.java
  #14 = NameAndType        #6:#7          //  "<init>":()V
  #15 = Utf8               PreIncrement2
  #16 = Utf8               java/lang/Object

ほら、#3がない! (>_<)

参考

ほかにもせっかく教えていただいたので書き留めておきます。こんなページ知りませんでした。

あとは、OpenJDKのjava_rw_demo.cのadd_new_cpool_entry関数ですね。

ひどいフォールダウンをかましているswitch文があるのですが、その中のLongとDoubleのところで、たしかにカウンタが1つ余分に進められています。

    switch (tag) {
        case JVM_CONSTANT_Class:
            writeU2(ci, index1);
            break;
        case JVM_CONSTANT_String:
            writeU2(ci, index1);
            break;
        case JVM_CONSTANT_Fieldref:
        case JVM_CONSTANT_Methodref:
        case JVM_CONSTANT_InterfaceMethodref:
        case JVM_CONSTANT_Integer:
        case JVM_CONSTANT_Float:
        case JVM_CONSTANT_NameAndType:
            writeU2(ci, index1);
            writeU2(ci, index2);
            break;
        case JVM_CONSTANT_Long:
        case JVM_CONSTANT_Double:
            writeU4(ci, index1);
            writeU4(ci, index2);
            ci->cpool_count_plus_one++;
            CRW_ASSERT(ci,  ci->cpool_count_plus_one < ci->cpool_max_elements );
            break;
        case JVM_CONSTANT_Utf8:
            CRW_ASSERT(ci, len==(len & 0xFFFF));
            writeU2(ci, len);
            write_bytes(ci, (void*)str, len);
            utf8 = (char*)duplicate(ci, str, len);
            break;
        default:
            CRW_FATAL(ci, "Unknown constant");
            break;
    }

(悩んだ時にこーいうのをバシッと見つけられるように、自分もなりたいものです)