【问题标题】:How to remove method body at runtime with ASM 5.2如何使用 ASM 5.2 在运行时删除方法体
【发布时间】:2017-12-24 03:19:55
【问题描述】:

我正在尝试在以下程序中删除 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;
        }

    }

}

【问题讨论】:

  • 你的代码应该如何影响班级?您所做的只是调用您自己实现为return null; 的方法MethodReplacer.visitMethod。一次总是只返回null 的方法调用怎么会对任何事情产生影响?
  • 哈哈我才意识到,现在我觉得自己很愚蠢。我不知道如何使用 ASM 库。任何帮助,将不胜感激。是不是该类已经加载并且我正在尝试修改它?

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


【解决方案1】:

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

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

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

不过,访问课程并不会改变它。它有助于在遇到某个工件时对其进行分析并执行操作。请注意,返回null 不是实际操作。

如果你想创建一个修改过的类,你需要一个ClassWriter 来生成这个类。 ClassWriter 实现了ClassVisitor,也实现了class visitors can be chained,因此您可以轻松地创建一个委托给编写者的自定义访问者,这将生成一个与原始文件相同的类文件,除非您重写一个方法来拦截一个功能。

但请注意,从 visitMethod 返回 null 不仅仅删除代码,它还会完全删除方法。相反,您必须为特定方法返回一个特殊访问者,该访问者将重现该方法但忽略旧代码并创建唯一的 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,它具有相同的名称(因为我们没有更改名称),但是与已经加载的类不同,因为它有不同的定义类加载器。我们只能通过反射来访问这样的动态生成的类:

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。考虑到复杂性,我认为很明显,实际的代码转换和将代码转换为运行时类或使用它来动态替换类,是两个必须协作的不同任务,但您通常需要其代码保持分开。

【讨论】:

  • 谢谢!我收到一个错误:ClassFormatError: Arguments can't fit into locals in class file 我尝试禁用的方法有 2 个参数。
  • 这段代码是针对问题的例子量身定做的,方法是static。如果不是,您必须更改术语(Type.getArgumentsAndReturnSizes(desc)&gt;&gt;2)-1 删除-1-1 正是为了删除this 引用的空间)。所以对于一个实例方法,Type.getArgumentsAndReturnSizes(desc)&gt;&gt;2 已经计算了‹this+arguments›的局部变量。
  • 好吧,COMPUTE_MAXS 强制 ASM 重新计算最大堆栈和本地值,因此它可以弥补这个地方的错误。我使用了super.visitMaxs(0, newMaxLocals),它应该是targetWriter.visitMaxs(0, newMaxLocals)。我在我的回答中修复了它,所以现在应该可以工作了,即使没有COMPUTE_MAXS
  • Class 获取Method 对象意味着它的加载。但请注意,对于 ASM,只需要名称和签名字符串来匹配方法。这些字符串在表达式new MethodReplacer(cw, method.getName(), Type.getMethodDescriptor(method)) 中提取,但您可以使用其他方式获取这些字符串。名称是最简单的部分,与您传递给Class.getMethod(…) 的字符串相同。描述符可以用Type.getMethodDescriptor(Type, Type...) 构造或者你学习the syntax...
  • 你可以在类中加入一个初始化器static { System.out.println("class initialized"); }。或者更好的static { Thread.dumpStack(); } 找到触发器......
【解决方案2】:

更简单的方法是创建一个 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());

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-28
    • 1970-01-01
    • 1970-01-01
    • 2011-10-22
    相关资源
    最近更新 更多