ASM:(9)Label


原文:https://lsieun.github.io/java-asm-01/label-intro.html

Label介绍

在Java程序中,有三种基本控制结构:顺序、选择和循环。

在Bytecode层面,只存在顺序(sequence)和跳转(jump)两种指令(Instruction)执行顺序:

                          ┌─── sequence
                          │
Bytecode: control flow ───┤
                          │                ┌─── selection (if, switch)
                          └─── jump ───────┤
                                           └─── looping (for, while)

那么,Label类起到一个什么样的作用呢?我们现在已经知道,MethodVisitor类是用于生成方法体的代码,

  • 如果没有Label类的参与,那么MethodVisitor类只能生成“顺序”结构的代码;
  • 如果有Label类的参与,那么MethodVisitor类就能生成“选择”和“循环”结构的代码。

Label类

Label类当中,定义了很多的字段和方法。为了方便,将Label类简化一下,内容如下:

public class Label {
    int bytecodeOffset;

    public Label() {
        // Nothing to do.
    }

    public int getOffset() {
        return bytecodeOffset;
    }
}

经过这样简单之后,Label类当中就只包含一个bytecodeOffset字段,那么这个字段代表什么含义呢?bytecodeOffset字段就是a position in the bytecode of a method。

举例子来说明一下。假如有一个test(boolean flag)方法,它包含的Instruction内容如下:

=== === ===  === === ===  === === ===
Method test:(Z)V
=== === ===  === === ===  === === ===
max_stack = 2
max_locals = 2
code_length = 24
code = 1B99000EB200021203B60004A7000BB200021205B60004B1
=== === ===  === === ===  === === ===
0000: iload_1              // 1B
0001: ifeq            14   // 99000E
0004: getstatic       #2   // B20002     || java/lang/System.out:Ljava/io/PrintStream;
0007: ldc             #3   // 1203       || value is true
0009: invokevirtual   #4   // B60004     || java/io/PrintStream.println:(Ljava/lang/String;)V
0012: goto            11   // A7000B
0015: getstatic       #2   // B20002     || java/lang/System.out:Ljava/io/PrintStream;
0018: ldc             #5   // 1205       || value is false
0020: invokevirtual   #4   // B60004     || java/io/PrintStream.println:(Ljava/lang/String;)V
0023: return               // B1
=== === ===  === === ===  === === ===
LocalVariableTable:
index  start_pc  length  name_and_type
    0         0      24  this:Lsample/HelloWorld;
    1         0      24  flag:Z

那么,Label类当中的bytecodeOffset字段,就表示当前Instruction“索引值”。

那么,这个bytecodeOffset字段是做什么用的呢?它用来计算一个“相对偏移量”。比如说,bytecodeOffset字段的值是15,它标识了getstatic指令的位置,而在索引值为1的位置是ifeq指令,ifeq后面跟的14,这个14就是一个“相对偏移量”。换一个角度来说,由于ifeq的索引位置是1,“相对偏移量”是14,那么1+14=15,也就是说,如果ifeq的条件成立,那么下一条执行的指令就是索引值为15getstatic指令了。

在ASM当中,Label类可以用于实现选择(if、switch)、循环(for、while)和try-catch语句。

在编写ASM代码的过程中,我们所要表达的是一种代码的跳转逻辑,就是从一个地方跳转到另外一个地方;在这两者之间,可以编写其它的代码逻辑,可能长一些,也可能短一些,所以,Instruction所对应的“索引值”还不确定。

Label类的出现,就是代表一个“抽象的位置”,也就是将来要跳转的目标。 当我们调用ClassWriter.toByteArray()方法时,这些ASM代码会被转换成byte[],在这个过程中,需要计算出Label对象中bytecodeOffset字段的值到底是多少,从而再进一步计算出跳转的相对偏移量(offset)。

如何使用Label类

从编写代码的角度来说,Label类是属于MethodVisitor类的一部分:通过调用MethodVisitor.visitLabel(Label)方法,来为代码逻辑添加一个潜在的“跳转目标”。

我们先来看一个简单的示例代码:

public class HelloWorld {
    public void test(boolean flag) {
        if (flag) {
            System.out.println("value is true");
        }
        else {
            System.out.println("value is false");
        }
        return;
    }
}

那么,test(boolean flag)方法对应的ASM代码如下:

public class LabelTest implements Opcodes {
    public static void main(String[] args) throws Exception {
        // (1) 生成byte[]内容
        byte[] bytes = dump();
        FileUtils.writeByteArrayToFile(new File("sample/HelloWord.class"), bytes);
    }

    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "test", "(Z)V", null, null);
        Label elseLabel = new Label();      // 首先,准备两个Label对象
        Label returnLabel = new Label();

        // 第1段
        mv.visitCode();
        mv.visitVarInsn(ILOAD, 1);
        mv.visitJumpInsn(IFEQ, elseLabel);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("value is true");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitJumpInsn(GOTO, returnLabel);

        // 第2段
        mv.visitLabel(elseLabel);         // 将第一个Label放到这里
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("value is false");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        // 第3段
        mv.visitLabel(returnLabel);      // 将第二个Label放到这里
        mv.visitInsn(RETURN);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
        return cw.toByteArray();
    }
}

如何使用Label类:

  • 首先,创建Label类的实例;
  • 其次,确定label的位置。通过MethodVisitor.visitLabel()方法,确定label的位置。
  • 最后,与label建立联系,实现程序的逻辑跳转。在条件合适的情况下,通过MethodVisitor类跳转相关的方法(例如,visitJumpInsn())与label建立联系。

Frame的变化

对于HelloWorld类中test()方法对应的Instruction内容如下:

public void test(boolean);
  Code:
     0: iload_1
     1: ifeq          15
     4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #3                  // String value is true
     9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: goto          23
    15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    18: ldc           #5                  // String value is false
    20: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    23: return

该方法对应的Frame变化情况如下:

test(Z)V
[sample/HelloWorld, int] []
[sample/HelloWorld, int] [int]
[sample/HelloWorld, int] []
[sample/HelloWorld, int] [java/io/PrintStream]
[sample/HelloWorld, int] [java/io/PrintStream, java/lang/String]
[sample/HelloWorld, int] []
[] []
[sample/HelloWorld, int] [java/io/PrintStream]                      // 注意,从上一行到这里是“非线性”的变化
[sample/HelloWorld, int] [java/io/PrintStream, java/lang/String]
[sample/HelloWorld, int] []
[] []

或者:

test:(Z)V
                               // {this, int} | {}
0000: iload_1                  // {this, int} | {int}
0001: ifeq            14       // {this, int} | {}
0004: getstatic       #2       // {this, int} | {PrintStream}
0007: ldc             #3       // {this, int} | {PrintStream, String}
0009: invokevirtual   #4       // {this, int} | {}
0012: goto            11       // {} | {}
                               // {this, int} | {}                 // 注意,从上一行到这里是“非线性”的变化
0015: getstatic       #2       // {this, int} | {PrintStream}
0018: ldc             #5       // {this, int} | {PrintStream, String}
0020: invokevirtual   #4       // {this, int} | {}
                               // {this, int} | {}
0023: return                   // {} | {}

通过上面的输出结果,可得出:由于程序代码逻辑发生了跳转(if-else),那么相应的local variables和operand stack结构也发生了“非线性”的变化。这部分内容与MethodVisitor.visitFrame()方法有关系。

示例

switch语句

实现switch语句可以使用lookupswitchtableswitch指令。

预期目标:

public class HelloWorld {
    public void test(int val) {
        switch (val) {
            case 1:
                System.out.println("val = 1");
                break;
            case 2:
                System.out.println("val = 2");
                break;
            case 3:
                System.out.println("val = 3");
                break;
            case 4:
                System.out.println("val = 4");
                break;
            default:
                System.out.println("val is unknown");
        }
    }
}

编码实现:

public class LabelTest2 implements Opcodes {
    public static void main(String[] args) throws Exception {
        // (1) 生成byte[]内容
        byte[] bytes = dump();
        FileUtils.writeByteArrayToFile(new File("sample/HelloWord.class"), bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(I)V", null, null);
            Label caseLabel1 = new Label();
            Label caseLabel2 = new Label();
            Label caseLabel3 = new Label();
            Label caseLabel4 = new Label();
            Label defaultLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            mv2.visitCode();
            mv2.visitVarInsn(ILOAD, 1);
            mv2.visitTableSwitchInsn(1, 4, defaultLabel, new Label[]{caseLabel1, caseLabel2, caseLabel3, caseLabel4});

            // 第2段
            mv2.visitLabel(caseLabel1);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 1");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第3段
            mv2.visitLabel(caseLabel2);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 2");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第4段
            mv2.visitLabel(caseLabel3);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 3");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第5段
            mv2.visitLabel(caseLabel4);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val = 4");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第6段
            mv2.visitLabel(defaultLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("val is unknown");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第7段
            mv2.visitLabel(returnLabel);
            mv2.visitInsn(RETURN);
            mv2.visitMaxs(0, 0);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();
    }
}

本示例当中,使用了MethodVisitor.visitTableSwitchInsn()方法,也可以使用MethodVisitor.visitLookupSwitchInsn()方法。

mv2.visitLookupSwitchInsn(defaultLabel, new int[]{1, 2, 3, 4}, new Label[]{caseLabel1, caseLabel2, caseLabel3, caseLabel4});

for语句

预期目标:

public class HelloWorld {
    public void test() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

编码实现:

public class LabelTest3 implements Opcodes {
    public static void main(String[] args) throws Exception {
        byte[] bytes = dump();
        FileUtils.writeByteArrayToFile(new File("sample/HelloWord.class"), bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor methodVisitor = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            Label conditionLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            methodVisitor.visitCode();
            methodVisitor.visitInsn(ICONST_0);
            methodVisitor.visitVarInsn(ISTORE, 1);

            // 第2段
            methodVisitor.visitLabel(conditionLabel);
            methodVisitor.visitVarInsn(ILOAD, 1);
            methodVisitor.visitIntInsn(BIPUSH, 10);
            methodVisitor.visitJumpInsn(IF_ICMPGE, returnLabel);
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitVarInsn(ILOAD, 1);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
            methodVisitor.visitIincInsn(1, 1);
            methodVisitor.visitJumpInsn(GOTO, conditionLabel);

            // 第3段
            methodVisitor.visitLabel(returnLabel);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(0, 0);
            methodVisitor.visitEnd();
        }

        cw.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();
    }
}

try-catch语句

预期目标:

public class HelloWorld {
    public void test() {
        try {
            System.out.println("Before Sleep");
            Thread.sleep(1000);
            System.out.println("After Sleep");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

编码实现:

    public static byte[] dump() throws Exception {

        // (1) 创建ClassWriter对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用visitXxx()方法
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
                null, "java/lang/Object", null);

        {
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(0, 0);
            mv1.visitEnd();
        }

        {
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
            Label startLabel = new Label();
            Label endLabel = new Label();
            Label exceptionHandlerLabel = new Label();
            Label returnLabel = new Label();

            // 第1段
            mv2.visitCode();
            // visitTryCatchBlock可以在这里访问
            mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException");

            // 第2段
            mv2.visitLabel(startLabel);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("Before Sleep");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv2.visitLdcInsn(new Long(1000L));
            mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv2.visitLdcInsn("After Sleep");
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            // 第3段
            mv2.visitLabel(endLabel);
            mv2.visitJumpInsn(GOTO, returnLabel);

            // 第4段
            mv2.visitLabel(exceptionHandlerLabel);
            mv2.visitVarInsn(ASTORE, 1);
            mv2.visitVarInsn(ALOAD, 1);
            mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);

            // 第5段
            mv2.visitLabel(returnLabel);
            mv2.visitInsn(RETURN);

            // 第6段
            // visitTryCatchBlock也可以在这里访问
            // mv2.visitTryCatchBlock(startLabel, endLabel, exceptionHandlerLabel, "java/lang/InterruptedException");
            mv2.visitMaxs(0, 0);
            mv2.visitEnd();
        }

        cw.visitEnd();

        // (3) 调用toByteArray()方法
        return cw.toByteArray();

    }

有一个问题,visitTryCatchBlock()方法为什么可以在后边的位置调用呢?这与Code属性的结构有关系:

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

因为instruction的内容(对应于visitXxxInsn()方法的调用)存储于Code结构当中的code[]内,而try-catch的内容(对应于visitTryCatchBlock()方法的调用),存储在Code结构当中的exception_table[]内,所以visitTryCatchBlock()方法的调用时机,可以早一点,也可以晚一点,只要整体上遵循MethodVisitor类对就于visitXxx()方法调用的顺序要求就可以了。

|                |          |     instruction     |
|                |  label1  |     instruction     |
|                |          |     instruction     |
|    try-catch   |  label2  |     instruction     |
|                |          |     instruction     |
|                |  label3  |     instruction     |
|                |          |     instruction     |
|                |  label4  |     instruction     |
|                |          |     instruction     |
asm