传入 ClassWriter 时 ClassNode.accept() 上的 IllegalArgumentException

问题描述

我对 ClassNode 所做的实际更改似乎已经奏效(据我所知),但是当我开始编写文件时,在下面指定的行上发生了 IllegalArgumentException。这没有多大意义,因为 ClassNode.accept() 接受一个 ClassVisitor 实例,而 ClassWriter 扩展了 ClassVisitor,如果有人对 asm 有更多经验,我们将不胜感激。

FileInputStream stream = new FileInputStream(filePath);

ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(stream);
classReader.accept(classNode,0);

for (MethodNode methodNode : classNode.methods){
    for (AbstractInsnNode abstractInsnNode : methodNode.instructions.toArray()){
        if (abstractInsnNode.getopcode() == Opcodes.LDC){
            LdcInsnNode ldc = (LdcInsnNode) abstractInsnNode;
            if (ldc.cst.toString().equals("[a-zA-Z0-9_]+")){
                ldc.cst = Pattern.compile("");
            }
        }
    }
}

ClassWriter classWriter = new ClassWriter(ClassWriter.COmpuTE_MAXS);
classNode.accept(classWriter); //Error Ocurs on this line
FileOutputStream outputStream = new FileOutputStream(filePath);
outputStream.write(classWriter.toByteArray());
outputStream.close();

完整的错误信息是:

Exception in thread "main" java.lang.IllegalArgumentException: value 
    at org.objectweb.asm.SymbolTable.addConstant(SymbolTable.java:501)
    at org.objectweb.asm.MethodWriter.visitLdcInsn(MethodWriter.java:1290)
    at org.objectweb.asm.tree.LdcInsnNode.accept(LdcInsnNode.java:66)
    at org.objectweb.asm.tree.InsnList.accept(InsnList.java:144)
    at org.objectweb.asm.tree.MethodNode.accept(MethodNode.java:792)
    at org.objectweb.asm.tree.MethodNode.accept(MethodNode.java:690)
    at org.objectweb.asm.tree.ClassNode.accept(ClassNode.java:426)
    at dev.bodner.jack.Main.main(Main.java:105)

我使用的是 java 16 和 asm 9.2

解决方法

当你想要检测像 Pattern.compile("[a-zA-Z0-9_]+") 这样的语句时,你必须意识到它会被编译成两条指令

ldc          "[a-zA-Z0-9_]+"         // pushing the string constant to the stack
invokestatic java/util/regex/Pattern compile (Ljava/lang/String;)Ljava/util/regex/Pattern;

因此,您只需更改由 ldc 指令推送的字符串。

例如

ClassNode classNode = new ClassNode();
try(FileInputStream stream = new FileInputStream(filePath)) {
    ClassReader classReader = new ClassReader(stream);
    classReader.accept(classNode,0);
}

for (MethodNode methodNode: classNode.methods) {
    for (AbstractInsnNode abstractInsnNode: methodNode.instructions) {
        if (abstractInsnNode.getOpcode() == Opcodes.LDC) {
            LdcInsnNode ldc = (LdcInsnNode) abstractInsnNode;
            if (ldc.cst.equals("[a-zA-Z0-9_]+")) {
                ldc.cst = ""; // the replacement string
            }
        }
    }
}

ClassWriter classWriter = new ClassWriter(0);
classNode.accept(classWriter);
try(FileOutputStream outputStream = new FileOutputStream(filePath)) {
    outputStream.write(classWriter.toByteArray());
}

这将替换任何出现的指令 ldc "[a-zA-Z0-9_]+",无论字符串如何使用。这对于您的用例来说可能已经足够了,但是如果您想确保您只是更改了立即传递给 Pattern.compile 的字符串,您可以将循环体更改为

if (node.getOpcode() == Opcodes.LDC && isPatternCompile(node.getNext())) {
    LdcInsnNode ldc = (LdcInsnNode) node;
    if (ldc.cst.equals("[a-zA-Z0-9_]+")) {
        ldc.cst = ""; // the replacement string
    }
}

介绍以下辅助方法:

private static boolean isPatternCompile(AbstractInsnNode n) {
    String expectedDesc;
    if(n.getOpcode() == Opcodes.INVOKESTATIC) {
        expectedDesc = "(Ljava/lang/String;)Ljava/util/regex/Pattern;";
    }
    else { // check for Pattern.compile(String,int) with int constant
        switch(n.getOpcode()) { 
            default: return false;
            case Opcodes.LDC:
                if(!((((LdcInsnNode)n).cst) instanceof Integer)) return false;
            case Opcodes.ICONST_0: case Opcodes.ICONST_1: case Opcodes.ICONST_2:
            case Opcodes.ICONST_3: case Opcodes.ICONST_4: case Opcodes.ICONST_M1:
            case Opcodes.BIPUSH: case Opcodes.SIPUSH:
        }
        n = n.getNext();
        if(n.getOpcode() != Opcodes.INVOKESTATIC) return false;
        expectedDesc = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;";
    }
    MethodInsnNode m = (MethodInsnNode) n;
    return m.name.equals("compile")
        && m.owner.equals("java/util/regex/Pattern")
        && m.desc.equals(expectedDesc);
}

请注意,当您决定只替换所有出现的特定字符串常量时,即无需检查后续说明,您可以直接使用 ASM 的访问者 API 来完成。 This answer 包含这样一个读取器和写入器链接的示例,它需要更少的内存,而且可能比使用 Tree API 更快。