Javaのバイトコードで遊ぶ (stack map frame)

Javaのバイトコードで遊ぶ (Hello World)

stack map frameについては以下を読みました。

これらを読んで、次のように理解しました。 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);
  }
}