如果在ClassWriter中设置了COMPUTE_FRAMES,则ASM跳过类

时间:2018-07-23 14:51:24

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

我一直在研究与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标志,则不会出现任何错误,但是某些类会被代理跳过并且不会进行转换。经过研究后,我发现其原因(很可能是)ClassWriter的getCommonSuperClass方法,该方法预先加载了该类。

如果我未设置COMPUTE_FRAMES标志,则会遇到Expected stackmap frame at this location错误,但无法解决。

有人对此问题有解决方案吗?

1 个答案:

答案 0 :(得分:0)

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

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

因此,原则上,应该仅使用原始帧即可。为此,请不要在COMPUTE_FRAMES中指定ClassWriter,也不要在SKIP_FRAMES中指定ClassReader。仅在原始大小小于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循环语句的位置相关的代码
  • 在描述循环结束分支目标的堆栈状态的visitFrame()之前报告“最后一行”,即第6行。 label_2既用于报告源代码行,又用作if_icmpge指令的目标。将visitLabel调用委派给ClassWriter时,您要定义分支目标,并且分支目标需要堆栈映射框架,因此visitLabel和{{1之间必须没有代码}}调用,但是您用来插入代码的visitFrame调用是在它们之间进行的。

解决方案:

  • 直接在visitLineNumber调用处插入代码以表示方法的开头。那是在其他任何事情发生之前,并且不会与任何后续操作冲突:

    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或除以零时,该方法可能不会被调用,但是您的原始代码有问题。

要在任意源代码行处注入代码,没有直接的解决方案。如图所示,源代码行未将1:1映射到字节码位置,并且报告的位置可能在无法注入的位置。最好选择其他条件,例如易于识别的代码结构(例如,已知的方法调用),在其前后插入。