如何使用ASM 5.2在运行时删除方法体

时间:2017-07-19 03:46:45

标签: java java-bytecode-asm bytecode-manipulation

我正在尝试删除以下程序中test()的方法体,以便不会向控制台打印任何内容。我正在使用ASM 5.2,但我尝试过的所有东西似乎都没有任何效果。

有人可以解释我做错了什么,并指出了一些关于ASM的最新教程或文档吗?我在Stackoverflow和ASM网站上发现的几乎所有内容都显得过时和/或无用。

public class BytecodeMods {

    public static void main(String[] args) throws Exception {
        disableMethod(BytecodeMods.class.getMethod("test"));
        test();
    }

    public static void test() {
        System.out.println("This is a test");
    }

    private static void disableMethod(Method method) {
        new MethodReplacer()
                .visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, method.getName(), Type.getMethodDescriptor(method), null, null);
    }

    public static class MethodReplacer extends ClassVisitor {

        public MethodReplacer() {
            super(Opcodes.ASM5);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            return null;
        }

    }

}

2 个答案:

答案 0 :(得分:8)

您不应该直接调用访问者的方法。

使用ClassVisitor的正确方法是使用您感兴趣的类的类文件字节创建ClassReader,并将类访问者传递给其accept方法。然后,类阅读器将根据类文件中的工件调用所有visit方法。

在这方面,您不应该考虑过时的文档,只是因为它引用了较旧的版本号。例如。 this document正确描述了该过程,并且它为图书馆说明版本2和版本5之间不需要进行根本性的更改。

尽管如此,参观课程并没有改变它。它有助于分析它并在遇到某个工件时执行操作。请注意,返回null不是实际操作。

如果要创建修改后的类,则需要ClassWriter来生成类。 ClassWriter实现了ClassVisitor,还有class visitors can be chained,因此您可以轻松创建委托给编写者的自定义访问者,这将生成与原始文件相同的类文件,除非您覆盖方法拦截特征的再创造。

但请注意,从null返回visitMethod不仅仅是删除代码,它还会完全删除该方法。相反,您必须返回特定方法的特殊访问者,该方法将重现该方法但忽略旧代码并创建唯一的return指令(您可以省略源代码中的最后return语句,但不是字节代码中的return指令。

private static byte[] disableMethod(Method method) {
    Class<?> theClass = method.getDeclaringClass();
    ClassReader cr;
    try { // use resource lookup to get the class bytes
        cr = new ClassReader(
            theClass.getResourceAsStream(theClass.getSimpleName()+".class"));
    } catch(IOException ex) {
        throw new IllegalStateException(ex);
    }
    // passing the ClassReader to the writer allows internal optimizations
    ClassWriter cw = new ClassWriter(cr, 0);
    cr.accept(new MethodReplacer(
            cw, method.getName(), Type.getMethodDescriptor(method)), 0);

    byte[] newCode = cw.toByteArray();
    return newCode;
}

static class MethodReplacer extends ClassVisitor {
    private final String hotMethodName, hotMethodDesc;

    MethodReplacer(ClassWriter cw, String name, String methodDescriptor) {
        super(Opcodes.ASM5, cw);
        hotMethodName = name;
        hotMethodDesc = methodDescriptor;
    }

    // invoked for every method
    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {

        if(!name.equals(hotMethodName) || !desc.equals(hotMethodDesc))
            // reproduce the methods we're not interested in, unchanged
            return super.visitMethod(access, name, desc, signature, exceptions);

        // alter the behavior for the specific method
        return new ReplaceWithEmptyBody(
            super.visitMethod(access, name, desc, signature, exceptions),
            (Type.getArgumentsAndReturnSizes(desc)>>2)-1);
    }
}
static class ReplaceWithEmptyBody extends MethodVisitor {
    private final MethodVisitor targetWriter;
    private final int newMaxLocals;

    ReplaceWithEmptyBody(MethodVisitor writer, int newMaxL) {
        // now, we're not passing the writer to the superclass for our radical changes
        super(Opcodes.ASM5);
        targetWriter = writer;
        newMaxLocals = newMaxL;
    }

    // we're only override the minimum to create a code attribute with a sole RETURN

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        targetWriter.visitMaxs(0, newMaxLocals);
    }

    @Override
    public void visitCode() {
        targetWriter.visitCode();
        targetWriter.visitInsn(Opcodes.RETURN);// our new code
    }

    @Override
    public void visitEnd() {
        targetWriter.visitEnd();
    }

    // the remaining methods just reproduce meta information,
    // annotations & parameter names

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return targetWriter.visitAnnotation(desc, visible);
    }

    @Override
    public void visitParameter(String name, int access) {
        targetWriter.visitParameter(name, access);
    }
}

自定义MethodVisitor不会链接到类编写器返回的方法访问者。以这种方式配置,它不会自动复制代码。相反,不执行任何操作将是默认操作,只有targetWriter上的显式调用才会生成代码。

在流程结束时,您有一个byte[]数组,其中包含类文件格式中已更改的代码。所以问题是,如何处理它。

您可以做的最简单,最便携的事情是创建一个新的ClassLoader,它会从这些字节创建一个新的Class,它具有相同的名称(因为我们没有更改name),但与已加载的类不同,因为它有一个不同的定义类加载器。我们只能通过Reflection:

访问这种动态生成的类
public class BytecodeMods {

    public static void main(String[] args) throws Exception {
        byte[] code = disableMethod(BytecodeMods.class.getMethod("test"));
        new ClassLoader() {
            Class<?> get() { return defineClass(null, code, 0, code.length); }
        }   .get()
            .getMethod("test").invoke(null);
    }

    public static void test() {
        System.out.println("This is a test");
    }

    …

为了使这个例子做一些比什么都不做更值得注意的事情,你可以改为改变消息,

使用以下MethodVisitor

static class ReplaceStringConstant extends MethodVisitor {
    private final String matchString, replaceWith;

    ReplaceStringConstant(MethodVisitor writer, String match, String replacement) {
        // now passing the writer to the superclass, as most code stays unchanged
        super(Opcodes.ASM5, writer);
        matchString = match;
        replaceWith = replacement;
    }

    @Override
    public void visitLdcInsn(Object cst) {
        super.visitLdcInsn(matchString.equals(cst)? replaceWith: cst);
    }
}

改变

        return new ReplaceWithEmptyBody(
            super.visitMethod(access, name, desc, signature, exceptions),
            (Type.getArgumentsAndReturnSizes(desc)>>2)-1);

        return new ReplaceStringConstant(
            super.visitMethod(access, name, desc, signature, exceptions),
            "This is a test", "This is a replacement");

如果要在加载到JVM之前更改已加载类的代码或拦截它,则必须使用Instrumentation API。

字节码转换本身不会改变,您必须将源字节传递到ClassReader并从ClassWriter返回修改后的字节。像ClassFileTransformer.transform(…)这样的方法已经接收到表示类的当前形式的字节(可能已经有过以前的转换)并返回新的字节。

问题是,这个API通常不适用于Java应用程序。它可用于所谓的Java代理,它必须是通过启动选项与JVM一起启动,或者以特定于实现的方式动态加载,例如,通过Attach API。

package documentation描述了Java代理的一般结构和相关的命令行选项。

this answer的末尾是一个程序,演示如何使用Attach API连接到您自己的JVM以加载虚拟Java代理,该代理将授予程序访问Instrumentation API的权限。考虑到复杂性,我认为,显而易见的是,实际的代码转换以及将代码转换为运行时类或使用它来动态替换类,是两个必须协作的不同任务,但是您通常需要的代码保持分离。

答案 1 :(得分:0)

更简单的方法是创建一个MethodNode实例并用新的InsnList替换主体。首先,您需要原始的类表示。你可以像@Holger建议的那样得到它。

Class<?> originalClass = method.getDeclaringClass();
ClassReader classReader;
try {
    cr = new ClassReader(
        originalClass.getResourceAsStream(originalClass.getSimpleName()+".class"));
} catch(IOException e) {
    throw new IllegalStateException(e);
}

然后创建一个ClassNode并替换方法体。

//Create the CLassNode
ClassNode classNode = new ClassNode();
classReader.accept(classNode,0);

//Search for the wanted method
final List<MethodNode> methods = classNode.methods;
for(MethodNode methodNode: methods){
    if(methodNode.name.equals("test")){
        //Replace the body with a RETURN opcode
        InsnList insnList = new InsnList();
        insnList.add(new InsnNode(Opcodes.RETURN));
        methodNode.instructions = insnList;
    }
}

在生成新类之前,您需要一个带有公共defineClass()方法的ClassLoader。就像这样。

public class GenericClassLoader extends ClassLoader {

    public Class<?> defineClass(String name, byte[] b) {
        return defineClass(name, b, 0, b.length);
    }

}

现在你可以生成实际的类了。

//Generate the Class
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
classNode.accept(classWriter);

//Define the representation
GenericClassLoader classLoader = new GenericClassLoader();
Class<?> modifiedClass = classLoader.defineClass(classNode.name, classWriter.toByteArray());