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

How to remove method body at runtime with ASM 5.2

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

谁能解释我做错了什么,并指出一些关于 ASM 的最新教程或文档?我在 Whosebug 和 ASM 网站上找到的几乎所有内容似乎都已过时 and/or 没有帮助。

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

    }

}

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

使用 ClassVisitor 的正确方法是创建 ClassReader with the class file bytes of the class you’re interested in and pass the class visitor to the its accept 方法。然后,所有 visit 方法将由 class reader 根据 class 文件中找到的工件调用。

在这方面,您不应认为文档已过时,只是因为它引用了较旧的版本号。例如。 this document 正确地描述了该过程,它代表库说明版本 2 和版本 5 之间没有必要进行根本性的更改。

仍然,访问 class 并没有改变它。它有助于分析它并在遇到某个工件时执行操作。请注意 returning null 不是实际操作。

如果你想创建一个修改后的 class,你需要一个 ClassWriter to produce the class. A ClassWriter implements ClassVisitor, also class visitors can be chained,这样你就可以轻松地创建一个委托给作者的自定义访问者,这将生成一个 class 文件与原始方法相同,除非您覆盖一种方法来拦截功能的重新创建。

但请注意,从 visitMethod returning null 不仅仅是删除代码,它会完全删除该方法。相反,您必须 return 特定方法的特殊访问者,它将重现该方法但忽略旧代码并创建唯一的 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 不会链接到方法访问者 return 由 class 作者编辑。以这种方式配置,它不会自动复制代码。相反,不执行任何操作将是默认设置,只有我们对 targetWriter 的显式调用才会生成代码。

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

你能做的最简单、最便携的事情是创建一个新的 ClassLoader,它从这些字节创建一个新的 Class,它具有相同的名称(因为我们没有更改名称),但与已加载的 class 不同,因为它具有不同的定义 class 加载程序。我们只能通过反射访问这样动态生成的class:

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");

如果你想更改已经加载的代码 class 或者在加载到 JVM 之前拦截它,你必须使用 Instrumentation API.

字节码转换本身并没有改变,您必须将源字节传递到 ClassReader 并从 ClassWriter 取回修改后的字节。 ClassFileTransformer.transform(…) 之类的方法已经接收到表示 class 当前形式的字节(可能有之前的转换)和 return 新字节。

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

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

this answer 的末尾是一个演示如何使用 Attach API 附加到您自己的 JVM 以加载虚拟 Java 代理程序的程序,该代理程序将为程序提供访问权限到仪表 API。考虑到复杂性,我认为,很明显,实际的代码转换和将代码转换为运行时 class 或使用它来动态替换 class 是两个不同的任务,必须协作,但您通常希望将其代码分开。

更简单的方法是创建一个 MethodNode 实例并用一个新的 InsnList 替换主体。首先,您需要原始的 class 表示。你可以像@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;
    }
}

在生成新的 class 之前,您需要一个带有 public defineClass() 方法的类加载器。就这样。

public class GenericClassLoader extends ClassLoader {

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

}

现在您可以生成实际的 class。

//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());