Javaのクラスファイルの中身を知る ConstantPoolテーブル

はじめに

Javaのプログラムがどうやって動いているのかを知りたい。
手始めに、javaのクラスファイルの中身がどうなっているか、特にプログラム中の各種名前(クラス/関数/フィールドなど)や固定値をどうやって格納しどうやってそれを参照しているかについて調べる。

対象読者:

  • Java(またはJava仮想マシンを対象とする言語)のプログラムを書いたことがある人
  • Javaのプログラムがどうやって動いているのかを知りたい人

Java仮想マシンJavaクラスファイルとはなにか

Java仮想マシンとは

Java仮想マシン (Java virtual machineJava VMJVM) は、Javaバイトコードとして定義された命令セットを実行するスタック型の仮想マシンAPIやいくつかのツールとセットでJava実行環境 (JRE) としてリリースされている。この環境を移植することで、さまざまな環境でJavaのプログラムを実行することができる。 (wikipediaより)

Javaクラスファイルとは

Javaクラスファイルは、Java仮想マシン (JVM) 上で実行可能なJavaバイトコードを含む(.class拡張子付きの)ファイルである。 (wikipediaより)

Javaバイトコードとは

Javaバイトコードは、Java仮想マシンが実行する命令形式である。各バイトコードのオペコードは長さが1バイトであるが、引数を持つものもあるため、結果として複数バイトの命令となる。 (wikipediaより)

オペコードとは

オペコード (operation code, opcode) とは、機械語の1個の命令の部分で、実行する操作 (operation) の種類を指定する部分のこと、およびそのコード(符号)のことである。数式における演算子に相当する。命令のもうひとつの主要部分は、操作される対象を指定するオペランド(被演算子)である。 (wikipediaより)

Java仮想マシンは、Javaクラスファイルの中身(Javaバイトコード)を解釈して動作する。
Javaバイトコードは、Java仮想マシンの命令である。
命令はオペコードと呼ばれる。オペコードの操作対象はオペランドと呼ばれる。

Javaソースコードコンパイルして実行するまでの流れ

f:id:masaki-linkode:20200124142633p:plain

Javaクラスファイルをダンプ出力

手っ取り早く体験するために、Javaで「Helloworld」を書いてコンパイルしてHelloWorld.classを作成し、それをダンプして中身を見てみる。

ソースコード

$ cat sample_data/HelloWorld.java
public class HelloWorld{
  public static void main(String[] args){
    System.out.println("Hello World!!");
  }
}

ダンプ出力結果

$ xxd -g8 sample_data/HelloWorld.class
00000000: cafebabe00000034 001d0a0006000f09  .......4........
00000010: 001000110800120a 0013001407001507  ................
00000020: 00160100063c696e 69743e0100032829  .....<init>...()
00000030: 56010004436f6465 01000f4c696e654e  V...Code...LineN
00000040: 756d626572546162 6c650100046d6169  umberTable...mai
00000050: 6e010016285b4c6a 6176612f6c616e67  n...([Ljava/lang
00000060: 2f537472696e673b 295601000a536f75  /String;)V...Sou
00000070: 72636546696c6501 000f48656c6c6f57  rceFile...HelloW
00000080: 6f726c642e6a6176 610c000700080700  orld.java.......
00000090: 170c001800190100 0d48656c6c6f2057  .........Hello W
000000a0: 6f726c6421210700 1a0c001b001c0100  orld!!..........
000000b0: 0a48656c6c6f576f 726c640100106a61  .HelloWorld...ja
000000c0: 76612f6c616e672f 4f626a6563740100  va/lang/Object..
000000d0: 106a6176612f6c61 6e672f5379737465  .java/lang/Syste
000000e0: 6d0100036f757401 00154c6a6176612f  m...out...Ljava/
000000f0: 696f2f5072696e74 53747265616d3b01  io/PrintStream;.
00000100: 00136a6176612f69 6f2f5072696e7453  ..java/io/PrintS
00000110: 747265616d010007 7072696e746c6e01  tream...println.
00000120: 0015284c6a617661 2f6c616e672f5374  ..(Ljava/lang/St
00000130: 72696e673b295600 2100050006000000  ring;)V.!.......
00000140: 0000020001000700 0800010009000000  ................
00000150: 1d00010001000000 052ab70001b10000  .........*......
00000160: 0001000a00000006 0001000000010009  ................
00000170: 000b000c00010009 0000002500020001  ...........%....
00000180: 00000009b2000212 03b60004b1000000  ................
00000190: 01000a0000000a00 0200000003000800  ................
000001a0: 040001000d000000 02000e            ...........

xxdコマンドを使って16進数で表示する。
文字列出力で来そうな箇所は文字列出力する。(右列)

■気になるキーワード

  • "<init>"
  • "()V"
  • "L"と";" (例:Ljava/lang/String;)
  • "["は見つかるが、"]"は無い。(例:[Ljava/lang/String;)
  • Code
  • LineNumberTable

javapコマンド

Java開発環境ではJavaクラスファイルの中身を見るためのツール(javap コマンド)を準備している。

javap とは

1つ以上のクラス・ファイルを逆アセンブルします。 (javap OracleDocsより)

アセンブルとは

アセンブラ(ぎゃくアセンブラ、英: disassembler; ディスアセンブラ)は、逆コンパイラの一種であるが、実行ファイルないしオブジェクトファイルの機械語コード(とシンボルテーブルなどの付随情報)を基に、アセンブリ言語ソースコードを生成する、すなわちアセンブラの逆の作用をするものを特に指す。

コンピュータが直接実行できるプログラムは数字の羅列である機械語であり、人間が直接理解することは困難である。この機械語は、人間にわかりやすいソースコードを、アセンブラコンパイラ、リンカといったソフトウェアによって機械的に変換して得られたものに過ぎないので、プログラマソースコードを理解してソフトウェアを開発すればよい。しかし、すでに機械語に変換されており、元のソースコードも手に入らない場合は、アセンブリとは逆の手順をたどる(逆アセンブリする)ことで擬似的にソースコードを復元することができる。 (wikipediaより)

JavaバイトコードJava仮想マシン機械語である。

Java仮想マシンの逆アセンブルコマンド(逆アセンブラ)であるjavapコマンドで人間にわかりやすいソースコードを生成する。

実行例

$ javap -c -p -constants sample_data/HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       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
}

■気になる点

ソースコードには無いキーワードがある。以下の通り。

  • "aload"
  • "invokespecial"
  • "geststatic"
  • "ldc"
  • "invokevirtual"

Java クラスファイルの構造

Javaクラスファイルのフォーマットはざっくり説明すると以下の通り。(wikipedia)

  • マジックナンバー
  • クラスファイルフォーマットのマイナーバージョン
  • クラスファイルフォーマットのメジャーバージョン
  • ConstantPoolテーブルエントリ数
  • ConstantPoolテーブル
  • アクセスフラグ
  • thisクラス識別子
  • スーパークラス識別子
  • インターフェーステーブルエントリ数
  • インターフェーステーブル
  • フィールドテーブルエントリ数
  • フィールドテーブル
  • メソッドテーブルエントリ数
  • メソッドテーブル
  • 属性テーブルエントリ数
  • 属性テーブル

Javaクラスファイルのフォーマット仕様に従って中身を見る

javapコマンドでJavaクラスファイルの中身を見ることができたが、ここではJavaクラスファイルのフォーマット仕様に従って頭から1バイトづつ取り出して中身を確認する。
Javaクラスファイルのフォーマット仕様に従ってファイルの内容を表示するプログラムをgo言語で実装する。(ソースコード)

実行例

$ go run main.go sample_data/HelloWorld.class
magicNumber: 0xcafebabe
minorVersion: 0000
majorVersion: 0052
constantPoolCount: 29
constantPoolRow#1: tag: ConstantMethodRef[0x000a], classIndex:6, nameAndTypeIndex:15
constantPoolRow#2: tag: ConstantFieldRef[0x0009], classIndex:16, nameAndTypeIndex:17
constantPoolRow#3: tag: ConstantString[0x0008], stringIndex:18
constantPoolRow#4: tag: ConstantMethodRef[0x000a], classIndex:19, nameAndTypeIndex:20
constantPoolRow#5: tag: ConstantClass[0x0007], nameIndex:21
constantPoolRow#6: tag: ConstantClass[0x0007], nameIndex:22
constantPoolRow#7: tag: ConstantUTF8[0x0001], length:6 content:<init>
constantPoolRow#8: tag: ConstantUTF8[0x0001], length:3 content:()V
constantPoolRow#9: tag: ConstantUTF8[0x0001], length:4 content:Code
constantPoolRow#10: tag: ConstantUTF8[0x0001], length:15 content:LineNumberTable
constantPoolRow#11: tag: ConstantUTF8[0x0001], length:4 content:main
constantPoolRow#12: tag: ConstantUTF8[0x0001], length:22 content:([Ljava/lang/String;)V
constantPoolRow#13: tag: ConstantUTF8[0x0001], length:10 content:SourceFile
constantPoolRow#14: tag: ConstantUTF8[0x0001], length:15 content:HelloWorld.java
constantPoolRow#15: tag: ConstantNameAndType[0x000c], nameIndex:7, descriptorIndex:8
constantPoolRow#16: tag: ConstantClass[0x0007], nameIndex:23
constantPoolRow#17: tag: ConstantNameAndType[0x000c], nameIndex:24, descriptorIndex:25
constantPoolRow#18: tag: ConstantUTF8[0x0001], length:13 content:Hello World!!
constantPoolRow#19: tag: ConstantClass[0x0007], nameIndex:26
constantPoolRow#20: tag: ConstantNameAndType[0x000c], nameIndex:27, descriptorIndex:28
constantPoolRow#21: tag: ConstantUTF8[0x0001], length:10 content:HelloWorld
constantPoolRow#22: tag: ConstantUTF8[0x0001], length:16 content:java/lang/Object
constantPoolRow#23: tag: ConstantUTF8[0x0001], length:16 content:java/lang/System
constantPoolRow#24: tag: ConstantUTF8[0x0001], length:3 content:out
constantPoolRow#25: tag: ConstantUTF8[0x0001], length:21 content:Ljava/io/PrintStream;
constantPoolRow#26: tag: ConstantUTF8[0x0001], length:19 content:java/io/PrintStream
constantPoolRow#27: tag: ConstantUTF8[0x0001], length:7 content:println
constantPoolRow#28: tag: ConstantUTF8[0x0001], length:21 content:(Ljava/lang/String;)V
AccessFlag: 0x0021
ThisClassIndex: 5
SuperClassIndex: 6
InterfacesCount: 0
FieldsCount: 0
MethodsCount: 2
method#0 methodAccessFlag: 0x0001, methodNameIndex: 7, descriptorIndex: 8, attributesCount: 1
  attributeNameIndex: 9, attributeLength: 29, maxStack: 1, maxLocals: 1, codeLength: 5,
op:
  < aload_0[0x002a] >
  < invokespecial[0x00b7] 0x0000 0x0001 >
  < return[0x00b1] >
, exceptionTableLength: 0, attributesCount: 1,   attributeNameIndex: 10, attributeLength: 6,
method#1 methodAccessFlag: 0x0009, methodNameIndex: 11, descriptorIndex: 12, attributesCount: 1
  attributeNameIndex: 9, attributeLength: 37, maxStack: 2, maxLocals: 1, codeLength: 9,
op:
  < getstatic[0x00b2] 0x0000 0x0002 >
  < ldc[0x0012] 0x0003 >
  < invokevirtual[0x00b6] 0x0000 0x0004 >
  < return[0x00b1] >
, exceptionTableLength: 0, attributesCount: 1,   attributeNameIndex: 10, attributeLength: 10,
attributesCount: 1

ConstantPoolテーブルとは

The Java virtual machine maintains a per-type constant pool (§3.5.5), a runtime data structure that serves many of the purposes of the symbol table of a conventional programming language implementation. (The JavaTM Virtual Machine Specification : The Runtime Constant Pool)

Java仮想マシンは、従来のプログラミング言語実装のシンボルテーブルの多くの目的に役立つ実行時データ構造である、タイプごとの定数プール(3.5 .5節)を維持する。(「みらい翻訳」で翻訳)

シンボルテーブルとは

シンボルテーブル(英: Symbol table)は、コンパイラインタプリタなどのようなコンピュータプログラミング言語処理系などのようなプログラムで使われるデータ構造であり、プログラムのソースコード内の変数名などといった名前(シンボル)と、それぞれの内容(データ型、スコープレベル、位置など)となるデータなどといった、「名前」→「中身」というような情報のテーブルである。 (wikipediaより)

一般的なコンパイラコンパイルして出力するオブジェクトファイル(今回の場合はJavaクラスファイル)はおおよそ以下のような構造になっている。

f:id:masaki-linkode:20200124142904p:plain

ConstantPoolテーブルエントリ数は2バイト。(エントリ数の最大65,535)。

ConstantPoolの値とその参照

ConstantPoolの1番目のエントリを見ると

constantPoolRow#1: tag: ConstantMethodRef[0x000a], classIndex:6, nameAndTypeIndex:15

とある。
1番目のエントリは6番目のエントリと15番目のエントリを参照していることを表す。

6番目のエントリを見ると

constantPoolRow#6: tag: ConstantClass[0x0007], nameIndex:22

とある。
6番目のエントリは22番目のエントリを参照していることを表す。

22番目のエントリを見ると

constantPoolRow#22: tag: ConstantUTF8[0x0001], length:16 content:java/lang/Object

とある。

ConstantPoolの参照を解決すると以下の通り。

  • constantPoolRow
    • 1: ConstantMethodRef
      • classIndex:6
        • ConstantClass
          • nameIndex:22
            • ConstantUTF8
      • nameAndTypeIndex:15
        • ConstantNameAndType
          • nameIndex:7
            • ConstantUTF8
              • <init>
          • descriptorIndex:8
            • ConstantUTF8
              • ()V
    • 2: ConstantFieldRef
      • classIndex:16
        • ConstantClass
          • nameIndex:23
            • ConstantUTF8
      • nameAndTypeIndex:17
        • ConstantNameAndType
          • nameIndex:24
            • ConstantUTF8
              • out
          • descriptorIndex:25
            • ConstantUTF8
              • Ljava/io/PrintStream;
    • 3: ConstantString
    • 4: ConstantMethodRef
      • classIndex:19
        • ConstantClass
          • nameIndex:26
            • ConstantUTF8
              • java/io/PrintStream
      • nameAndTypeIndex:20
        • ConstantNameAndType
          • nameIndex:27
            • ConstantUTF8
              • println
          • descriptorIndex:28
            • ConstantUTF8
              • (Ljava/lang/String;)V
    • 5: ConstantClass
      • nameIndex:21
        • ConstantUTF8
          • HelloWorld
    • 6: ConstantClass
      • nameIndex:22
        • ConstantUTF8

コード(命令)部分からConstantPoolの値を参照

後半の

  • ThisClassIndex
  • SuperClassIndex
  • method#0
  • method#1

に注目する。

ThisClassIndex、SuperClassIndex

ThisClassIndex:5

  • ConstantClass
    • name: HelloWorld

SuperClassIndex: 6

  • ConstantClass

であることがわかる。

method

命令セット仕様を読むとわかるが、method#0、method#1に含まれるオペコードのうち、オペランドを持つものは

  • invokespecial
  • getstatic
  • ldc
  • invokevirtual

である。(aload_0オペランドを持たない)

invokespecialについて仕様のinvokespecialを読むと

Invoke instance method; special handling for superclass, private, and instance initialization method invocations

Invoke instanceメソッド;スーパークラス、プライベート、およびインスタンスの初期化メソッド呼び出しの特別な処理(「みらい翻訳」で翻訳)

The unsigned indexbyte1 and indexbyte2 are used to construct an index into the run-time constant pool of the current class (§2.6), where the value of the index is (indexbyte1 << 8) | indexbyte2. The run-time constant pool item at that index must be a symbolic reference to a method or an interface method (§5.1), which gives the name and descriptor (§4.3.3) of the method as well as a symbolic reference to the class or interface in which the method is to be found. The named method is resolved (§5.4.3.3, §5.4.3.4).

unsigned indexbyte1およびindexbyte2は、インデックスの値が(indexバイト1<<8)|indexbyte2である現在のクラス(§2.6)の実行時定数プールにインデックスを構築するために使用されます。そのインデックスの実行時定数プール項目は、メソッドまたはインタフェースメソッド(§5.1)へのシンボリック参照である必要があり、メソッドの名前と記述子(4.3 .3節)、およびメソッドが検索されるクラスまたはインタフェースへのシンボリック参照を与えます。指定されたメソッドが解決されます(§5.4.3.3、§5.4.3.4)。(「みらい翻訳」で翻訳)

とある。

invokespecialのオペランドは「0x0000 0x0001」。 ConstantPoolのエントリ#1(0 << 8 | 1の結果)のメソッド名(とそのクラス名)を指しているっぽい。

オペランドのConstantPool#1を調べてみると

  • ConstantMethodRef
    • ConstantClass
    • ConstantNameAndType
      • name
        • <init>
      • descriptor
        • ()V

これはjavapの出力である、

invokespecial #1                  // Method java/lang/Object."<init>":()V

のコメントの内容と一致している。

同じようにgetstaticオペランド はConstantPool#2である

  • ConstantFieldRef
    • ConstantClass
      • name
    • ConstantNameAndType
      • name
        • out
      • descriptor
        • Ljava/io/PrintStream;

を指す。

これはjavapの出力である

getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

のコメントの内容と一致している。

ldcオペランド はConstantPool#3である

これはjavapの出力である

ldc           #3                  // String Hello World!!

のコメントの内容と一致している。 (仕様を読むとわかるがldcオペランドは1バイト。ldc_wというのもありそのオペランドは2バイト。)

invokevirtualオペランド はConstantPool#4である

  • ConstantMethodRef
    • ConstantClass
      • name
        • java/io/PrintStream
    • ConstantNameAndType
      • name
        • println
      • descriptor
        • (Ljava/lang/String;)V

これはjavapの出力である

invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

のコメントの内容と一致している。

まとめ

Javaクラスファイルの中にはクラス名、関数名、フィールド名を構造的に格納するシンボルテーブルがある。
Javaクラスファイルの中にはJava仮想マシンの命令があり、シンボルテーブルのエントリを参照している。

おまけ

Javaプログラマは、Javaバイトコードについて全く知ったり理解する必要はない。しかしながら、IBMdeveloperWorksによると、「バイトコードを理解することと、どんなバイトコードJavaコンパイラにより生成される可能性が高いを理解することは、アセンブリ言語の知識がCやC++プログラマの助けになるのと同じように、Javaプログラマの助けになる」とされている。(wikipediaより)