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
的条件成立,那么下一条执行的指令就是索引值为15
的getstatic
指令了。
在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语句可以使用lookupswitch
或tableswitch
指令。
预期目标:
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 |