如何移除 Instruction
在修改方法体的代码时,如何移除一条 Instruction 呢?其实,很简单,就是让中间的某一个 MethodVisitor
对象不向后“传递该 instruction”就可以了。
但是,需要要注意一点:无论是添加 instruction,还是删除 instruction,还是要替换 instruction,都要保持 operand stack 修改前和修改后是一致的。这句话该怎么理解呢?我们举个例子来进行说明。
假如,有一条打印语句,如下:
System.out.println("Hello World");
这条打印语句,对应着三个 instruction,如下:
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Hello World"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
上面三条 instruction 的执行过程如下:
- 第一步,执行
GETSTATIC java/lang/System.out: Ljava/io/PrintStream;
,会把一个System.out
对象 push 到 operand stack 上。 - 第二步,执行
LDC "Hello World"
,会将一个字符串对象 push 到 operand stack 上。 - 第三步,执行
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
,会消耗掉 operand stack 栈顶上的两个元素,然后打印出结果。
如果我们只想删除第三条 INVOKEVIRTUAL
对应的 instruction,是不行的,因为它会让 operand stack 栈顶上多出两个元素。这三条 instruction 应该一起删除,才能保证 operand stack 在修改前和修改后是一致的。
示例:移除 NOP
为了让示例容易一些,我们先来处理一个比较简单的情况,那就是移除 NOP
指令。那么,为什么要移除 NOP
指令呢?因为 NOP
表示 no operation,它是一个单独的指令,本身也不做什么操作,我们删除它不会影响任何实质性的操作,也不会牵连其它的 instruction。
当然,一般情况下,由 .java
编译生成的 .class
文件中不会包含 NOP
指令。那么,我们就自己生成一个 .class
文件,让它带有 NOP
指令。
预期目标
我们想实现的预期目标:删除代码当中的 NOP
指令。
首先,我们来生成一个包含 NOP
指令的 .class
文件,如下:
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
import static org.objectweb.asm.Opcodes.*;
public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
// (1) 生成 byte[] 内容
byte[] bytes = dump();
// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, 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, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}
{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
mv2.visitCode();
mv2.visitInsn(NOP);
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitInsn(NOP);
mv2.visitLdcInsn("Hello World");
mv2.visitInsn(NOP);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv2.visitInsn(NOP);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 1);
mv2.visitEnd();
}
cw.visitEnd();
// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}
查看生成后的效果:
$ javap -c sample.HelloWorld
public class sample.HelloWorld {
public sample.HelloWorld();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: nop
1: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
4: nop
5: ldc #17 // String Hello World
7: nop
8: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: nop
12: return
}
编码实现
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class MethodRemoveNopVisitor extends ClassVisitor {
public MethodRemoveNopVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name) && !"<clinit>".equals(name)) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
mv = new MethodRemoveNopAdapter(api, mv);
}
}
return mv;
}
private static class MethodRemoveNopAdapter extends MethodVisitor {
public MethodRemoveNopAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitInsn(int opcode) {
// if (opcode == NOP) {
// // do nothing
// }
// else {
// super.visitInsn(opcode);
// }
if (opcode != NOP) {
super.visitInsn(opcode);
}
}
}
}
进行转换
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)构建 ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串连 ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new MethodRemoveNopVisitor(api, cw);
//(4)结合 ClassReader 和 ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成 byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
验证结果
$ javap -c sample.HelloWorld
public class sample.HelloWorld {
public sample.HelloWorld();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #17 // String Hello World
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
总结
本文主要对移除 Instruction 进行了介绍,内容总结如下:
- 第一点,移除 Instruction 方式,就是让中间的某一个
MethodVisitor
对象不向后“传递该 instruction”就可以了。 - 第二点,在移除 instruction 的过程中,要保证 operand stack 在修改前和修改后是一致的。
在后面的内容当中,我们会介绍到如何删除打印语句,因为它要经历一个“模式识别”的过程,相对复杂一些,所以我们放到后面的内容再来讨论。