Javaのバイトコードで遊ぶ (stack map frame)
stack map frameについては以下を読みました。
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.4
- http://download.forge.objectweb.org/asm/asm4-guide.pdf
- java - Is there a better explanation of stack map frames? - Stack Overflow
これらを読んで、次のように理解しました。 stack map frameとは、バイトコードのある位置におけるローカル変数とスタックの型の状態を示すもので、VMの各命令が扱う値が正しい型かを検証するために使われます。 全ての型を推論することは難しく、クラスローディングを遅くしてしまうため、その解決のために導入されたようです。 クラスファイルのサイズが大きくなりすぎないようフレームで型を明示する位置はジャンプのターゲットに絞っており、またフレームの差分で指定できるようにしています。
各stack map frameはバイトコードのある位置(bytecode offset)における型の状態を表します。 bytecode offsetを算出するために各stack map frameはoffset_deltaという値を持ちます。 あるstack map frameが示すbytecode offsetは、1つ前のstack map frameのbytecode offset+offset_delta+1で求められます。 ただし1つ前のframeがinitial frame(メソッド開始時のframe)の場合(つまりbytecode offset=0)、offset_deltaの値がそのままbytecode offsetになります。
次にstack map frameの種類についてです。
same_frame
frameの種類を示すframe_typeの値が[0-63]のものがこれに該当します。 このframeは1つ前のframeとローカル変数が同じでスタックが空であることを意味します。 offset_deltaはframe_typeと同じ値になります。(frame_type=0ならoffset_delat=0)
same_locals_1_stack_item_frame
frame_typeの値が[64-127]のものがこれに該当します。 このframeは1つ前のframeとローカル変数が同じでスタックに1つ値が積まれていることを意味します。 offset_deltaはframe_type-64になります。(frame_type=64ならoffset_delat=0)
same_locals_1_stack_item_frame_extended
frame_typeの値が247のものがこれに該当します。(frame_type=[128-246]は将来のために予約されています) このframeはsame_locals_1_stack_item_frameと同じで、1つ前のframeとローカル変数が同じでスタックに1つ値が積まれていることを意味します。 明示的にoffset_deltaを与える必要がある点がsame_locals_1_stack_item_frameと異なります。
chop_frame
frame_typeの値が[248-250]のものがこれに該当します。 このframeはスタックが空で、1つ前のframeからいくつかのローカル変数が削除されていることを意味します。 削除されているローカル変数の数は、251-frame_typeで求められます。(frame_type=248なら削除されたローカル変数は3つ) 明示的にoffset_deltaを与える必要があります。
same_frame_extended
frame_typeの値が251のものがこれに該当します。 意味するところはsame_frameと同じですが、明示的にoffset_deltaを与える必要がある点で異なります。
append_frame
frame_typeの値が[252-254]のものがこれに該当します。 このframeはスタックが空で、1つ前のframeからいくつかのローカル変数が追加されていることを意味します。 追加されているローカル変数の数は、frame_type-251で求められます。(frame_type=252なら削除されたローカル変数は1つ) 明示的にoffset_deltaを与える必要があります。
full_frame
frame_typeの値が255のものがこれに該当します。 ローカル変数、スタックの状態を全て明示的に示す必要があります。 offset_deltaも明示します。
実際のバイトコードにてstack map frameを確認します。javaコードは以下です。
int a = 1; if (a > 0) { System.out.println("x"); } if (a > 0) { float b = 2.0F; System.out.println("y"); } else { long c = 3L; if (c > 0) { double d = 4.0; } System.out.println("z"); }
これは以下のバイトコードになります。
stack=4, locals=6, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: ifle 14 6: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #21 // String x 11: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: iload_1 15: ifle 31 18: fconst_2 19: fstore_2 20: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 23: ldc #29 // String y 25: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: goto 54 31: ldc2_w #31 // long 3l 34: lstore_2 35: lload_2 36: lconst_0 37: lcmp 38: ifle 46 41: ldc2_w #33 // double 4.0d 44: dstore 4 46: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 49: ldc #35 // String z 51: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 54: return ... StackMapTable: number_of_entries = 4 frame_type = 252 /* append */ offset_delta = 14 locals = [ int ] frame_type = 16 /* same */ frame_type = 252 /* append */ offset_delta = 14 locals = [ long ] frame_type = 250 /* chop */ offset_delta = 7
それでは順にstack map frameを見ていきます。
frame_type = 252 /* append */ offset_delta = 14 locals = [ int ]
initial frameの次のframeのためbytecode offsetはoffset_deltaである14。
このbytecode offsetは3: ifle 14
からのジャンプのターゲットと一致します。
14: iload_1
においてローカル変数にintが1つ(252-251)増えていることを示します。
frame_type = 16 /* same */
bytecode offsetは31(14+16+1)です。
31: ldc2_w #31
においてローカル変数に前のframeから変化がないことを示します。
frame_type = 252 /* append */ offset_delta = 14 locals = [ long ]
bytecode offsetは46(31+14+1)です。
46: getstatic #15
においてローカル変数にlongが1つ(252-251)増えていることを示します。
frame_type = 250 /* chop */ offset_delta = 7
bytecode offsetは54(46+7+1)です。
54: return
においてローカル変数が1つ(251-250)減っていることを示します。
このstack map frameをASMで指定するにはMethodVisitor.visitFrameを用います。
visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack)
typeには、F_NEW, F_FULL, F_SAME, F_SAME1, F_APPEND, F_CHOPを指定できます。
前述のframe_typeとは若干異なりますが、名前から対応付けはわかります。 F_NEWはClassReaderで使うもののようなのでClassWriterでは不要。
nLocal以降はtypeに応じてスタックとローカル変数の内容を指定します。
このvisitFrameを使ってFizzBuzzを書き直してみました。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class FizzBuzzWithFrame implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); Label loopBegin = new Label(); Label loopNext = new Label(); Label loopEnd = new Label(); Label fizzBranch = new Label(); Label buzzBranch = new Label(); Label elseBranch = new Label(); // i = 1 int _i = 1; mv.visitInsn(ICONST_1); mv.visitVarInsn(ISTORE, _i); // max = 100 int _max = 2; mv.visitIntInsn(BIPUSH, 100); mv.visitVarInsn(ISTORE, _max); // -- loopBegin -- mv.visitLabel(loopBegin); // ループ開始時と2回目以降でローカル変数が異なる // (j, kが追加)のでF_APPEND mv.visitFrame(F_APPEND, 2, new Object[] { Opcodes.INTEGER, Opcodes.INTEGER }, 0, null); // j = i % 3 int _j = 3; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_3); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _j); // k = i % 5 int _k = 4; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_5); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _k); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // if j == 0 && k == 0 then print "FizzBuzz" mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitLdcInsn("FizzBuzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if j == 0 then print "Fizz" mv.visitLabel(fizzBranch); // メソッド開始時のフレームとだいぶ差がある // (ローカル変数にi, max, j, k、 スタックにSystem.out)ので // F_FULLで全部指定 mv.visitFrame(F_FULL, 5, new Object[] { "[Ljava/lang/String;", Opcodes.INTEGER, Opcodes.INTEGER, Opcodes.INTEGER, Opcodes.INTEGER }, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, buzzBranch); mv.visitLdcInsn("Fizz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if k == 0 then print "Buzz" mv.visitLabel(buzzBranch); // 前フレームと差はないがスタックに1つ値があるのでF_SAME1 mv.visitFrame(F_SAME1, 0, null, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, elseBranch); mv.visitLdcInsn("Buzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // else print i mv.visitLabel(elseBranch); // 前フレームと差はないがスタックに1つ値があるのでF_SAME1 mv.visitFrame(F_SAME1, 0, null, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _i); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // -- loopNext -- mv.visitLabel(loopNext); // 前フレームと同一、スタックも空なのでF_SAME mv.visitFrame(F_SAME, 0, null, 0, null); // i++ mv.visitIincInsn(_i, 1); // if i <= max then goto loopBegin mv.visitVarInsn(ILOAD, _i); mv.visitVarInsn(ILOAD, _max); mv.visitJumpInsn(IF_ICMPLE, loopBegin); // -- loopEnd -- mv.visitLabel(loopEnd); mv.visitInsn(RETURN); mv.visitMaxs(2, 5); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "FizzBuzzWithFrame"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }