Edited at

PHP で Java の中間コードの class ファイルを生成して、 java コマンドで Hello World を出力する


はじめに

今までこういう記事を書いてきました

そして、こういう内容で登壇してたりもしました。

ということで、これらの逆のパターンをやろうと思います。

つまり、 PHP で Java の中間コードを生成して、java コマンドで Hello World を出力するといった内容です。


どうやるのか?

とっても簡単で、 JVM Spec と呼ばれる Oracle 公式が出しているドキュメントに則って、 PHP でバイナリを書き出して行くだけです。


早速やる


バイナリを書き込む用のクラスを用意する

PHP はバイナリを書くには少し不便なので、予めバイナリを書きやすいように専用のクラスを用意します。今回は下記を使用します。

class StreamWriter

{
private $handle;

public function __construct($handle)
{
$this->handle = $handle;
}

public function writeUnsignedShort(int $number)
{
return $this->writeBytes(pack('n', $number));
}

public function writeUnsignedInt(int $number)
{
return $this->writeBytes(pack('N', $number));
}

public function writeUnsignedChar(int $number)
{
return $this->writeBytes(pack('C', $number));
}

public function writeBytes(string $bytes)
{
fwrite($this->handle, $bytes);
return $this;
}

public function readAll()
{
rewind($this->handle);
return stream_get_contents($this->handle);
}
}

軽くそれぞれ説明します。


writeUnsignedShort

short, つまり 16 bit, 言い換えると 2 バイトの値をストリームに書き込みます。

pack 関数の nunsigned short でビッグエンディアンを指しています。

class ファイルは基本的にビッグエンディアンで書き込んでいきます。


writeUnsignedInt

int, つまり 32 bit, 言い換えると 4 バイトの値をストリームに書き込みます。

pack 関数の Nunsigned int でビッグエンディアンを指しています。


writeUnsignedChar

char, つまり 8 bit, 言い換えると 1 バイトの値をストリームに書き込みます。

pack 関数の Cunsigned char でビッグエンディアンを指しています。


writeBytes

指定した文字列をそのままストリームに書き込みます。


readAll

書き込んだストリームを取得します。


書き込む必要のあるクラスの構造について

Java の class の構造は下記のようになっています。


ClassFile Structure

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 は 2 bytes という意味ですが、扱う意図のほとんどが unsigned short のため、unsigned short として紹介します。

u4 も上記と同様ですが、 4 bytes であるため、 unsigned int として扱います。

これらを書き込んでいきます。


ストリームを用意する

書き込み先のストリームを用意します。

$writer = new StreamWriter(fopen('Test.class', 'w+'));


magic を書き込む

magic は別名フォーマット識別子ともいい、対象のファイルがどういったフォーマットなのかを示しておくためのものです。

これは PNG や GIF, JPEG などでも見られます。

Java のフォーマット識別子は 0xCAFEBABE であるため、これをストリームに書き込みます。

$writer->writeUnsignedInt(0xCAFEBABE);

上記のように書き込んでいきます。


minor_version を書き込む

Java のマイナーバージョンを書き込みます。基本的には 0 で OK です。

$writer->writeUnsignedShort(0);


major_version を書き込む

Java のメジャーバージョンを書き込みます。今回は JDK 8 を対象にします。

JVM Spec より、下記の表より JDK 8 はどれに該当するかを調べます。

class file format
version range

1.0.2
45.0 <= v <= 45.3

1.1
45.0 <= v <= 45.65535

1.2
45.0 <= v <= 46.0

1.3
45.0 <= v <= 47.0

1.4
45.0 <= v <= 48.0

5.0
45.0 <= v <= 49.0

6
45.0 <= v <= 50.0

7
45.0 <= v <= 51.0

8
45.0 <= v <= 52.0

9
45.0 <= v <= 53.0

10
45.0 <= v <= 54.0

11
45.0 <= v <= 55.0

JDK 8 は 45.0 <= v <= 52.0 と書かれているので、 52 と書き込みます。

$writer->writeUnsignedShort(52);


constant_pool_count を書き込む

Constant Pool の数を書き込みます。 Constant Pool とは、 JVM 上におけるシンボルテーブルのような役割で、クラス、メソッド・フィールドの参照情報、文字列とのリンクなどの情報が格納されており、 JVM の要といっても過言ではありません。

さて、今回追加する Constant Pool ですが、主に Hello World に必要なものだけを書き込みたいと思います。

ただ、初回だと難しいため、 Hello World が書かれているTest.javajavac でコンパイルし、 javap で必要な Constant Pool を参考にしながら、追加します。

javap の結果は下記のとおりです。

Classfile /path/to/Test.class

Last modified Sep 2, 2019; size 415 bytes
MD5 checksum d06ed1ee0d14b88eb1a0d6dda5bd6a22
Compiled from "Test.java"
class Test
minor version: 0
major version: 52
flags: (0x0020) ACC_SUPER
this_class: #5 // Test
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
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 JS-JVM!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Test
#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 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello JS-JVM!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Test
#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
{
Test();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) 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 JS-JVM!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "Test.java"

上記が javap の結果で、今回見るのは Constant Pool の箇所です。

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 JS-JVM!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Test
#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 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello JS-JVM!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Test
#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

これの個数をこのセクション、実際の値を次のセクションで書いていきます。

個数は # の横の数字を見れば分かる通り、 1 から 28 であるため、 29個 です。

$this->writeUnsignedShort(29);


constant_pool[constant_pool_count-1] を書き込む

Constant Poolの実際のエントリを書き込んでいきます。さて、上記のセクションで出てきているもので、 Utf8Class と書かれているものを総称して Tag と呼称します。

さて、この Tag は下記の 6 つ出てきています。


  • Class (0x07)

  • Fieldref (0x09)

  • Methodref (0x0A)

  • String (0x08)

  • NameAndType (0x0C)

  • Utf8 (0x01)

それぞれどういった情報を書き込めばいいのかを下記に示します。

JVM 上では Tag のプリフィックスとして、 CONSTANT_ がついています。


CONSTANT_Class

CONSTANT_Class_info {

u1 tag;
u2 name_index;
}


CONSTANT_Fieldref

CONSTANT_Fieldref_info {

u1 tag;
u2 class_index;
u2 name_and_type_index;
}


CONSTANT_Methodref

CONSTANT_Methodref_info {

u1 tag;
u2 class_index;
u2 name_and_type_index;
}


CONSTANT_NameAndType


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


CONSTANT_String

CONSTANT_String_info {

u1 tag;
u2 string_index;
}


CONSTANT_Utf8

CONSTANT_Utf8_info {

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

これらの値をストリームに書き込んでいきます。

// #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V

$writer->writeUnsignedChar(0x0A);
$writer->writeUnsignedShort(6);
$writer->writeUnsignedShort(15);

// #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
$writer->writeUnsignedChar(0x09);
$writer->writeUnsignedShort(16);
$writer->writeUnsignedShort(17);

// #3 = String #18 // Hello JS-JVM!
$writer->writeUnsignedChar(0x08);
$writer->writeUnsignedShort(18);

// #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
$writer->writeUnsignedChar(0x0A);
$writer->writeUnsignedShort(19);
$writer->writeUnsignedShort(20);

// #5 = Class #21 // Test
$writer->writeUnsignedChar(0x07);
$writer->writeUnsignedShort(21);

// #6 = Class #22 // java/lang/Object
$writer->writeUnsignedChar(0x07);
$writer->writeUnsignedShort(22);

// #7 = Utf8 <init>
$writer->writeUnsignedChar(0x01);
$text = '<init>';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #8 = Utf8 ()V
$writer->writeUnsignedChar(0x01);
$text = '()V';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #9 = Utf8 Code
$writer->writeUnsignedChar(0x01);
$text = 'Code';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #10 = Utf8 LineNumberTable
$writer->writeUnsignedChar(0x01);
$text = 'LineNumberTable';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #11 = Utf8 main
$writer->writeUnsignedChar(0x01);
$text = 'main';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #12 = Utf8 ([Ljava/lang/String;)V
$writer->writeUnsignedChar(0x01);
$text = '([Ljava/lang/String;)V';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #13 = Utf8 SourceFile
$writer->writeUnsignedChar(0x01);
$text = 'SourceFile';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #14 = Utf8 Test.java
$writer->writeUnsignedChar(0x01);
$text = 'Test.java';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #15 = NameAndType #7:#8 // "<init>":()V
$writer->writeUnsignedChar(0x0C);
$writer->writeUnsignedShort(7);
$writer->writeUnsignedShort(8);

// #16 = Class #23 // java/lang/System
$writer->writeUnsignedChar(0x07);
$writer->writeUnsignedShort(23);

// #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
$writer->writeUnsignedChar(0x0C);
$writer->writeUnsignedShort(24);
$writer->writeUnsignedShort(25);

// #18 = Utf8 Hello JS-JVM!
$writer->writeUnsignedChar(0x01);
$text = 'Hello PHP Compiler!';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #19 = Class #26 // java/io/PrintStream
$writer->writeUnsignedChar(0x07);
$writer->writeUnsignedShort(26);

// #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
$writer->writeUnsignedChar(0x0C);
$writer->writeUnsignedShort(27);
$writer->writeUnsignedShort(28);

// #21 = Utf8 Test
$writer->writeUnsignedChar(0x01);
$text = 'Test';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #22 = Utf8 java/lang/Object
$writer->writeUnsignedChar(0x01);
$text = 'java/lang/Object';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #23 = Utf8 java/lang/System
$writer->writeUnsignedChar(0x01);
$text = 'java/lang/System';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #24 = Utf8 out
$writer->writeUnsignedChar(0x01);
$text = 'out';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #25 = Utf8 Ljava/io/PrintStream;
$writer->writeUnsignedChar(0x01);
$text = 'Ljava/io/PrintStream;';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #26 = Utf8 java/io/PrintStream
$writer->writeUnsignedChar(0x01);
$text = 'java/io/PrintStream';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #27 = Utf8 println
$writer->writeUnsignedChar(0x01);
$text = 'println';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);

// #28 = Utf8 (Ljava/lang/String;)V
$writer->writeUnsignedChar(0x01);
$text = '(Ljava/lang/String;)V';
$writer->writeUnsignedShort(strlen($text));
$writer->writeBytes($text);


access_flags を書き込む

クラスに対するアクセスフラグ、つまりアクセス修飾子を書き込みます。

アクセス修飾子として書き込むフラグは下記のとおりです。

Flag Name
Value
Interpretation

ACC_PUBLIC
0x0001
Declared public; may be accessed from outside its package.

ACC_FINAL
0x0010
Declared final; no subclasses allowed.

ACC_SUPER
0x0020
Treat superclass methods specially when invoked by the invokespecial instruction.

ACC_INTERFACE
0x0200
Is an interface, not a class.

ACC_ABSTRACT
0x0400
Declared abstract; must not be instantiated.

ACC_SYNTHETIC
0x1000
Declared synthetic; not present in the source code.

ACC_ANNOTATION
0x2000
Declared as an annotation type.

ACC_ENUM
0x4000
Declared as an enum type.

ACC_MODULE
0x8000
Is a module, not a class or interface.

引用: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.1

継承は特にしていないので、 ACC_SUPER (0x0020) つまり、親クラスとしてのアクセス修飾子を書き込みます。

$writer->writeUnsignedShort(0x0020);


this_class を書き込む

現在のクラスの情報を書き込みます。これは Constant PoolTagClass のものを指します。今回のクラスは Test なので、 Constant Pool5 を書き込みます。

$writer->writeUnsignedShort(5);


super_class を書き込む

親クラスの情報を書き込みます。これも上記同様 Constant PoolTagClass のものを指します。

extends をつけていない場合、慣習的に java.lang.Object となるため、 Constant Pool6 を書き込みます。

$writer->writeUnsignedShort(6);


interfaces_count を書き込む

実装されているインタフェースの個数を書き込みますが、特に実装していないため、 0 を書き込みます。

$writer->writeUnsignedShort(0);


fields_count を書き込む

現在のクラスで定義しているフィールドの個数を書き込みますが、こちらも特に定義していないので、 0 を書き込みます。

$writer->writeUnsignedShort(0);


methods_count を書き込む

Hello World を出力するためには 2 つのメソッドが必要です。それぞれ、コンストラクタと main メソッドが必要になります。コンストラクタは定義していなくても通常は <init> という名前でコンパイル時に中間コードに追加されます。

$writer->writeUnsignedShort(2);


methods[methods_count] を書き込む

上述の通り、 <init>main を書き込んでいきます。

メソッドのエントリは JVM Spec より下記のとおりになっています。

method_info {

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

これを書き込んでいきます。


メソッド


access_flags

コンストラクタへのアクセス修飾子を指定します。

アクセス修飾子の種類は JVM Spec より下記のとおりです。

Flag Name
Value
Interpretation

ACC_PUBLIC
0x0001
Declared public; may be accessed from outside its package.

ACC_PRIVATE
0x0002
Declared private; accessible only within the defining class and other classes belonging to the same nest (§5.4.4).

ACC_PROTECTED
0x0004
Declared protected; may be accessed within subclasses.

ACC_STATIC
0x0008
Declared static.

ACC_FINAL
0x0010
Declared final; must not be overridden (§5.4.5).

ACC_SYNCHRONIZED
0x0020
Declared synchronized; invocation is wrapped by a monitor use.

ACC_BRIDGE
0x0040
A bridge method, generated by the compiler.

ACC_VARARGS
0x0080
Declared with variable number of arguments.

ACC_NATIVE
0x0100
Declared native; implemented in a language other than the Java programming language.

ACC_ABSTRACT
0x0400
Declared abstract; no implementation is provided.

ACC_STRICT
0x0800
Declared strictfp; floating-point mode is FP-strict.

ACC_SYNTHETIC
0x1000
Declared synthetic; not present in the source code.

コンストラクタは public で問題ないので、 0x0001 を書き込みます。

$writer->writeUnsignedShort(0x0001);


name_index

コンストラクタの名称である <init> の Constant Pool の番号を指します。

今回の場合上の方に書いているの Constant Pool より 7 を指定します。

$writer->writeUnsignedShort(7);


descriptor_index

メソッドの引数及び返却値の型情報を格納しているディスクリプタを指定します。

これも Constant Pool の番号を示しており、特段引数を取ったりしておらず、かつ返却値は void であるため 8 となります。

$writer->writeUnsignedShort(8);


attributes_count

一般的に Hello World の出力を javac でコンパイルするとオペレーションコードの情報が格納されている Code Attribute と LineNumber Attribute が格納され、一般的には 2 となりますが、 LineNumber Attribute は特に必須ではないので、 Code Attribute のみ書き込みます。したがって、このセクションでは 1 を書き込みます。

$writer->writeUnsignedShort(1);


attributes[attributes_count]

Code Attribute を書き込んでいきます。 Code Attribute のフォーマットは JVM Spec より下記のとおりとなります。

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

Code Attribute の Code の部分の Constant Pool の番号を指します。

今回の場合は 9 となります。

$writer->writeUnsignedShort(9);


attribute_length

Attribute 全体の長さを書き込みます。書き込む値を知るには、予めオペレーションコードがどれくらいの長さになるかを知る必要があるため、まず、オペレーションコードをつくります。

特にコードを指定していない <init> は一般的に親クラスである java.lang.Object のコンストラクタである <init> をコールします。

<init> をコールするのに必要なオペレーションコードは aload_0, invokespecial, return3 つです。

aload_0 はコール対象のオブジェクトをローカル変数としてロード(=読み込む)します。この場合は、初期化済みの Test クラスを渡します。 では、なぜ Test なのかというと、 JVM において static ではないクラスは第一パラメータにバインドされているクラスの情報を渡します。高級言語で言えば、いわゆるコンストラクタ・インジェクションに近いことを行っています。

そして、ロードされた変数をオペランドスタックと呼ばれる JVM のメモリ上に積みます。

(これをプッシュといいます。)

次に、 invokespecial では、オペランドスタックに積まれたコール元、上でいうと aload_0 でロードされた Test クラスをオペランドスタックから取得します。(これをポップといいます。)

また、オペランドスタックには引数の情報もありますが、 今回の <init> は引数を取らないため、特に考慮する必要はありません。

そのあと、呼び出し先のクラスの情報及びメソッドの参照情報 (Methodref) の Constant Pool の番号をオペランドとして持ちます。

そして、ポップした値と、引数、実行させるクラスの情報 (今回の場合 java.lang.Object ) とメソッドの情報 (main) でメソッドの呼び出しを行います。

その後、 return でオペレーションの終了を示します。

コードで表すと、下記のとおりになります。

また、それぞれのオペレーションコードは下記のとおりとなります。

Mnemonic
OpCode
Operands (bytes)

aload_0
0x2A
none

invokespecial
0xB7
2

return
0xB1
none

この情報をもとにするとオペコードは下記のようになります。

2A B7 00 01 B1

つまり、 Code の長さは 5 となります。これを attribute length としての長さに加えてあげることにより、正確な長さがでます。

$writer->writeUnsignedInt(

2 + // max_stack
2 + // max_locals
4 + // code_length
5 + // コードの長さ
2 + // exception_table_length
2 + // attributes_count
0
);


max_stack

オペランドスタックに渡される最大の個数を書き込みます。オペランドスタックに積む処理は aload_0 のみなので、 1 です。たぶん 999 とかでも大丈夫。

$writer->writeUnsignedShort(1);


max_locals

ローカル変数の数を示します。 aload_0 つまり、 Test という値は予めローカル変数として定義されているため (いわゆる this と同等) 1 となります。

$writer->writeUnsignedShort(1);


code_length

オペレーションコードの長さを書き込みます。上で既に求めているように 5 を書き込みます。

$writer->writeUnsignedInt(5);


code[code_length]

上記のコードを書き込みます。オペレーションコードは 1 byte (unsigned char) で、それにオペランドが付く場合はオペランドを書き込んでいきます。

// aload_0

$writer->writeUnsignedChar(0x2A);

// invokespecial
$writer->writeUnsignedChar(0xB7);
//// invokespecial - operand
$writer->writeUnsignedShort(1);

// return
$writer->writeUnsignedChar(0xB1);


exception_table_length

例外の情報を保持しているテーブルですが、例外は特に発生しないので 0 を書き込みます。

$writer->writeUnsignedShort(0);


attributes_count

特に付く 属性情報はないので 0 を書き込みます。

$writer->writeUnsignedShort(0);


main メソッド


access_flags

main メソッドの場合 public (0x0001) かつ static (0x0008) であるため、 | を用いて両方が含まれていることを書き込みます。

$writer->writeUnsignedShort(0x0001 | 0x0008);


name_index

main が書かれている Constant Pool11 なので、当該の値を書き込みます。

$writer->writeUnsignedShort(11);


descriptor_index

main メソッドは引数に String[] args を取るため、これに該当する Constant Pool の番号である 12 を書き込みます。

$writer->writeUnsignedShort(12);


attributes_count

これも <init> 同様に、 Code Attribute があるため、 1 を書き込みます。

$writer->writeUnsignedShort(1);


attributes[attributes_count]


attribute_name_index

main メソッドも 9 となります。

$writer->writeUnsignedShort(9);


attribute_length

Hello World を出力するには、メソッドの呼び出し元である System.out, 呼び出すメソッドである println が必要です。

System.out は名前の通りで、かつ静的なフィールドであるため、 getstatic を用いて取得します。

println は呼び出すメソッドとして invokevirtual と呼ばれるオペレーションコードのオペランドとして持っています。

また、 invokevirtual 実行時に System.out.println に渡す引数の情報が必要で、これが Hello World と呼ばれる値となります。

この値を取得するためにConstant Pool の値をそのままオペランドスタックに積めるオペレーションコードの ldc が必要になります。

Mnemonic
OpCode
Operands (bytes)

getstatic
0xB2
2

ldc
0x12
1

invokevirtual
0xB6
2

return
0xB1
none

この情報をもとにするとオペコードは下記のようになります。

B2 00 02 12 00 03 B6 00 04 B1

つまり、 Code の長さは 10 となります。これを attribute length としての長さに加えてあげることにより、正確な長さがでます。

$writer->writeUnsignedInt(

2 + // max_stack
2 + // max_locals
4 + // code_length
10 + // コードの長さ
2 + // exception_table_length
2 + // attributes_count
0
);


max_stack

getstatic, ldc により最大 2 件のオペランドスタックが積まれるため、 2 を書き込みます。

$writer->writeUnsignedShort(2);


max_locals

String[] args という引数がローカル変数として扱われるため、 1 と書き込みます。

$writer->writeUnsignedShort(1);


code_length

オペレーションコードの長さを書き込みます。上で既に求めているように 10 を書き込みます。

$writer->writeUnsignedInt(10);


code[code_length]

下記のようになります。

// getstatic

$writer->writeUnsignedChar(0xB2);
$writer->writeUnsignedShort(2);

// ldc
$writer->writeUnsignedChar(0x12);
$writer->writeUnsignedChar(3);

// invokevirtual
$writer->writeUnsignedChar(0xB6);
$writer->writeUnsignedShort(4);

// return
$writer->writeUnsignedChar(0xB1);


exception_table_length

<init> 同様 例外は特に発生しないので 0 を書き込みます。

$writer->writeUnsignedShort(0);


attributes_count

これも属性情報はないので 0 を書き込みます。

$writer->writeUnsignedShort(0);


attributes_count を書き込む

Sourcefile という中間コードにもともとのコンパイル元のファイル名を書き込みます。

これのみなので、このセクションでは 1 をストリームに書き込みます。

$writer->writeUnsignedShort(1);


attributes[attributes_count] を書き込む

Sourcefile Attribute の形式は下記のとおりです。

SourceFile_attribute {

u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}

引用: https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.10

Test.java をもともとのファイル名として扱うため、 Constant Pool14 を書き込みます。

// Sourcefile

$writer->writeUnsignedShort(13);

// Attribute Length (sourcefile_index が 2 bytes なので 2)
$writer->writeUnsignedInt(2);

// sourcefile_index
$writer->writeUnsignedShort(14);


上手くいくと Hello World が出力されます。