0

我一直在研究与 maven-surfire-plugin 一起运行的 Java 代理。代理应该能够在三个不同的点将使用 ASM 库的方法调用注入到加载的方法中: 1) 在每个方法的开头;2) 在每个方法结束时;3) 在某些行(见下文)。为此,我实现了一个 premain 方法,它为 Java 工具添加了一个新的转换器。然后,transform 方法为它应该转换的每个类创建一个新的 ClassWriter 和 ClassVisitor(属于 ASM 库)。

@Override
public void visitLineNumber(int line, Label start) {
    if(methodLines.first().equals(line)) {
        mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
    }

    if(mutationLines != null && mutationLines.contains(line)) {
        mv.visitLdcInsn(fqn);
        mv.visitLdcInsn(new Integer(line));
        mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "hitMutation", "(Ljava/lang/String;I)V", false);
    }

    mv.visitLineNumber(line, start);

    if(methodLines.last().equals(line)) {
        mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
    }
}

不幸的是,我遇到了一些麻烦。如果设置了COMPUTE_FRAMES标志ClassWriter,我不会收到任何错误,但是代理会跳过某些类并且不会对其进行转换。经过一番研究,我发现造成这种情况的原因(很可能)是getCommonSuperClassClassWriter 的方法,它预先加载了类。

如果我不设置COMPUTE_FRAMES标志,我会收到Expected stackmap frame at this location无法解决的错误。

有人有解决这个问题的方法吗?

4

1 回答 1

1

正如这个答案中所解释的,ASM 计算(最具体的)公共超类的方法不一定会重现原始类的堆栈图帧。它不仅需要访问类(您可以解决),还可以访问原始代码从未引用过的类,因为原始代码使用了更抽象的类型或接口类型,或者因为原始框架实际上删除了随后未使用的值,而不是声明合并类型。

因此,更可取的方法是根据您所做的代码修改,根据原始帧计算堆栈图帧。对于您的预期用例,这很容易,因为您没有更改代码的分支结构,而只是注入代码,使堆栈状态与插入的代码片段之前完全相同。

所以原则上,应该可以只使用原始帧。要实现这一点,不要指定COMPUTE_FRAMESClassWriter并且不要指定SKIP_FRAMESClassReader. 如果原始大小小于 2,您只需调整最大堆栈大小,以确保您的方法参数有空间。

您的代理的实际问题来自尝试使用源代码行来确定插入调用的代码位置。为了说明这一点,请考虑以下示例:

public class Example {
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}

我使用以下代码来显示哪些 ASM 调用将发送给您的访问者:

public static void main(String[] args) throws IOException {
    ClassReader cr = new ClassReader("Example");
    cr.accept(new ClassVisitor(Opcodes.ASM5) {
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            System.out.println(name+desc);
            return new PrintingVisitor();
        }
    }, 0);
}
static class PrintingVisitor extends MethodVisitor {
    final Map<Label,Integer> labels = new HashMap<>();

    public PrintingVisitor() {
        super(Opcodes.ASM5);
    }
    private String name(Label label) {
        return "label_"+labels.merge(label, labels.size(), (a,b) -> a);
    }
    @Override public void visitCode() {
        System.out.println("visitCode()");
    }
    @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
        System.out.println("visitFrame()");
    }
    @Override public void visitLabel(Label label) {
        System.out.println("."+name(label));
    }
    @Override public void visitLineNumber(int line, Label start) {
        System.out.println(".line "+line+", "+name(start));
    }
    @Override public void visitJumpInsn(int opcode, Label label) {
        System.out.println(get(opcode)+" "+name(label));
    }
    @Override public void visitInsn(int opcode) {
        System.out.println(get(opcode));
    }
    @Override
    public void visitIincInsn(int var, int increment) {
        System.out.println("iinc "+var+", "+increment);
    }
    @Override public void visitEnd() {
        System.out.println();
    }
}
static String get(int opcode) {
    // for simplification, just the ones we need
    switch(opcode) {
        case Opcodes.RETURN: return "return";
        case Opcodes.ICONST_0: return "iconst_0";
        case Opcodes.ILOAD: return "iload";
        case Opcodes.IF_ICMPGE: return "if_icmpge";
        case Opcodes.GOTO: return "goto";
        default: return "<"+opcode+">";
    }
}

产生(用 编译时javac):

main([Ljava/lang/String;)V
visitCode()
.label_0
.line 3, label_0
iconst_0
.label_1
visitFrame()
if_icmpge label_2
.label_3
.line 4, label_3
.label_4
.line 3, label_4
iinc 1, 1
goto label_1
.label_2
.line 6, label_2
visitFrame()
return
.label_5

这表明:

  • “第一行”,即第 3 行被报告两次,因为循环在其末尾生成与for循环语句的位置相关联的代码
  • “最后一行”,即第6行在visitFrame()描述循环结束的分支目标的堆栈状态之前报告。label_2用于报告源代码行和作为if_icmpge指令的目标。将visitLabel调用委托给 时,您正在定义分支目标,并且分支目标需要堆栈图框架,因此and调用ClassWriter之间必须没有代码,但是您用来插入代码的调用是在它们之间进行的.visitLabelvisitFramevisitLineNumber

解决方案:

  • visitCode()在方法开始的调用处注入代码。那是在其他任何事情发生之前,并且不会与任何后续操作发生冲突:

    @Override public void visitCode() {
        super.visitCode();
        mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
    }
    
  • 对于在方法末尾注入代码,只需使用可以结束方法的精确指令,即

    @Override public void visitInsn(int opcode) {
        switch(opcode) {
            case RETURN: case ARETURN: case IRETURN: case LRETURN: case FRETURN: case DRETURN:
            case ATHROW:
                mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
        }
        super.visitInsn(opcode);
    }
    

    请注意,这不足以获得finally在每种情况下都调用该方法的类似语义。例如,当调用的方法抛出异常或运行时生成异常时,就像取消引用null或除以零时一样,该方法可能不会被调用,但您的原始代码存在问题。

对于在任意源代码行注入代码,没有直接的解决方案。如图所示,源代码行不会 1:1 映射到字节码位置,并且报告的位置可能位于无法注入的位置。最好选择一个附加标准,如易于识别的代码结构,例如,一个已知的方法调用,插入到它之前或之后。

于 2018-07-25T18:35:57.530 回答