What am I doing

我使用ASM和javaagent来检测类以报告其覆盖率(为什么我不使用jacoco?这与这个问题无关),基本逻辑是,每次调用visitLineNumber时,我检测一些方法调用(就在访问下一条指令之前)以记录命中行号.

Problem description

通过这样一个简单的逻辑,一个类得到了ClassFormatError:

java.lang.ClassFormatError: StackMapTable format error: bad offset for Uninitialized in method org.apache.commons.math.ode.ContinuousOutputModelTest.buildInterpolator(D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    ...

插装前的字节码如下所示.堆栈映射帧所在的指令为@16(offset\u delta=16)和@17(offset\u delta=0).

private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double);
    descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
    flags: ACC_PRIVATE
    Code:
      stack=7, locals=7, args_size=4
         0: new           #66                 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator
         3: dup
         4: aload_3
         5: dload         4
         7: dload_1
         8: dcmpl
         9: iflt          16
        12: iconst_1
        13: goto          17
        16: iconst_0
        17: invokespecial #67                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V
        20: astore        6
        22: aload         6
        24: dload_1
        25: invokevirtual #68                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
        28: aload         6
        30: invokevirtual #69                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V
        33: aload         6
        35: dload         4
        37: invokevirtual #68                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
        40: aload         6
        42: areturn
         ...
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 16
          locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
          stack = [ uninitialized 0, uninitialized 0, class "[D" ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
          stack = [ uninitialized 0, uninitialized 0, class "[D", int ]

插装后,字节码变为:

private org.apache.commons.math.ode.sampling.StepInterpolator buildInterpolator(double, double[], double);
    descriptor: (D[DD)Lorg/apache/commons/math/ode/sampling/StepInterpolator;
    flags: ACC_PRIVATE
    Code:
      stack=10, locals=7, args_size=4
         0: ldc_w         #264                // String org/apache/commons/math/ode/ContinuousOutputModelTest
         3: ldc_w         #344                // String buildInterpolator
         6: ldc_w         #345                // int 169
         9: invokestatic  #272                // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
        12: new           #66                 // class org/apache/commons/math/ode/sampling/DummyStepInterpolator
        15: dup
        16: aload_3
        17: dload         4
        19: dload_1
        20: dcmpl
        21: iflt          28
        24: iconst_1
        25: goto          29
        28: iconst_0
        29: invokespecial #67                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator."<init>":([DZ)V
        32: astore        6
        34: ldc_w         #264                // String org/apache/commons/math/ode/ContinuousOutputModelTest
        37: ldc_w         #344                // String buildInterpolator
        40: ldc_w         #346                // int 170
        43: invokestatic  #272                // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
        46: aload         6
        48: dload_1
        49: invokevirtual #68                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
        52: ldc_w         #264                // String org/apache/commons/math/ode/ContinuousOutputModelTest
        55: ldc_w         #344                // String buildInterpolator
        58: ldc_w         #347                // int 171
        61: invokestatic  #272                // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
        64: aload         6
        66: invokevirtual #69                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.shift:()V
        69: ldc_w         #264                // String org/apache/commons/math/ode/ContinuousOutputModelTest
        72: ldc_w         #344                // String buildInterpolator
        75: ldc_w         #348                // int 172
        78: invokestatic  #272                // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
        81: aload         6
        83: dload         4
        85: invokevirtual #68                 // Method org/apache/commons/math/ode/sampling/DummyStepInterpolator.storeTime:(D)V
        88: ldc_w         #264                // String org/apache/commons/math/ode/ContinuousOutputModelTest
        91: ldc_w         #344                // String buildInterpolator
        94: ldc_w         #349                // int 173
        97: invokestatic  #272                // Method org/test/cov/CoverageCollector.reportCoverage:(Ljava/lang/String;Ljava/lang/String;I)V
       100: aload         6
       102: areturn
        ...
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 28
          locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
          stack = [ uninitialized 0, uninitialized 0, class "[D" ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class org/apache/commons/math/ode/ContinuousOutputModelTest, double, class "[D", double ]
          stack = [ uninitialized 0, uninitialized 0, class "[D", int ]

我没有发现StackMapTable有任何问题.你知道为什么StackMapTable格式无效吗?


以下是我的检测代码:


class CoverageMethodVisitor extends MethodVisitor {

    private String slashClassName;
    private String methodName;
    private int currentLine;
    private boolean isJUnit3TestClass;
    private boolean hasTestAnnotation;
    private boolean isTestMethod;
    private int classVersion;

    private boolean isRightAfterLabel;

    protected CoverageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName, boolean isJUnit3TestClass, int classVersion) {
        super(ASM_VERSION, methodVisitor);
        this.slashClassName = className;
        this.methodName = methodName;
        this.isJUnit3TestClass = isJUnit3TestClass;
        this.classVersion = classVersion;
    }

    private void instrumentReportCoverageInvocation() {
        super.visitLdcInsn(slashClassName);
        super.visitLdcInsn(methodName);
        super.visitLdcInsn(currentLine);
        super.visitMethodInsn(INVOKESTATIC, "org/test/cov/CoverageCollector",
                "reportCoverage", "(Ljava/lang/String;Ljava/lang/String;I)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitIntInsn(opcode, operand);
    }

    @Override
    public void visitVarInsn(int opcode, int varIndex) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitVarInsn(opcode, varIndex);
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitTypeInsn(opcode, type);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitFieldInsn(opcode, owner, name, descriptor);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }

    @Override
    public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
    }

    @Override
    public void visitJumpInsn(int opcode, Label label) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitJumpInsn(opcode, label);
    }

    @Override
    public void visitLdcInsn(Object value) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitLdcInsn(value);
    }

    @Override
    public void visitIincInsn(int varIndex, int increment) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitIincInsn(varIndex, increment);
    }

    @Override
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }

    @Override
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }

    @Override
    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        if (isRightAfterLabel) instrumentReportCoverageInvocation(); isRightAfterLabel = false;
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack+3, maxLocals);
    }

    /**
     * Should not report line coverage immediately after the visitLineNumber. visitLineNumber is called right after
     * visitLabel, but it is very possible that a stack map frame is after the label, if insert instructions right
     * after the label, the original stack map frame will be messed up. So instead, insert instructions before the
     * first instruction after the label. */
    @Override
    public void visitLineNumber(int line, Label start) {
        super.visitLineNumber(line, start);
        currentLine = line;
        isRightAfterLabel = true;
    }
}

/** */中的 comments 实际上是我的猜测,不确定是否正确.


EDIT:

很抱歉,我误解了101的含义,因为java spec提到:

帧应用的字节码偏移量是通过将offset_delta+1添加到前一帧的字节码偏移量来计算的,除非前一帧是该方法的初始帧...

Summary

Reason of the error

bad offset for Uninitialized中的Uninitialized表示由NEW指令(原始字节码中的第一条指令)生成的未初始化对象.由于原始堆栈映射帧需要使用NEW指令之前的标签(比如L0)来表示其localsstack中的未初始化对象,并且插入指令的代码中的L0不再表示NEW指令,因此引发了此类错误.

Solution

由于插入指令的代码中的L0不再代表NEW指令,我们需要为该NEW指令创建一个新标签,并在两个堆栈映射帧中用新标签替换旧标签L0.如果指定了COMPUTE_FRAME,这样的堆栈映射帧(re)计算将自动完成,但这里我们需要手动完成,因为COMPUTE_FRAME不用于避免其他潜在问题.

推荐答案

作为Rafael Winterhalter said,标签用于引用NEW条指令,因此将指令放置在标签位置和指令之间将打破此引用.当指令创建的未初始化实例位于堆栈上或局部变量中时,堆栈映射帧需要此类引用.

由于在NEW指令之前插入代码时希望将原始位置保留为分支目标或行号开始,因此必须为插入的代码和旧的NEW指令之间的代码位置创建新标签,然后替换堆栈映射帧使用的标签(并且仅替换堆栈映射帧使用的标签).

请注意,当使用COMPUTE_FRAMES选项时,这是不必要的,COMPUTE_FRAMES选项将从头开始重新计算帧,并且不使用旧标签,然而,不使用此选项实际上是一种合理的策略,因为它成本高昂且容易出错.对于注入没有分支的简单日志(log)语句,我们可以保留原始帧,即使在判断NEW条指令的特殊情况时,这会稍微困难一些.

下面的方法visitor实现了上述策略.在测试中,我只注入了一个普通的System.out.println(…);语句.

class CoverageMethodVisitor extends MethodVisitor {
    private final String slashClassName;
    private final String methodName;
    private final Map<Label,Label> translateForUninitialized = new HashMap<>();
    private Label lastLabel;
    private int newLineNumber = -1;

    protected CoverageMethodVisitor(MethodVisitor mv,String clName,String methodName){
        super(Opcodes.ASM9, mv);
        this.slashClassName = clName;
        this.methodName = methodName;
    }

    @Override
    public void visitLineNumber(int line, Label start) {
        newLineNumber = line;
        super.visitLineNumber(line, start);
    }

    @Override
    public void visitLabel(Label label) {
        lastLabel = label;
        super.visitLabel(label);
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
        if(instrumentReportCoverageInvocation() && opcode == Opcodes.NEW) {
            if(lastLabel != null) {
                Label newLabel = new Label();
                super.visitLabel(newLabel);
                translateForUninitialized.put(lastLabel, newLabel);
            }
        }
        super.visitTypeInsn(opcode, type);
    }

    @Override
    public void visitFrame(int type,
        int numLocal, Object[] local, int numStack, Object[] stack) {
        switch(type) {
            case Opcodes.F_NEW, Opcodes.F_FULL -> {
                local = replaceLabels(numLocal, local);
                stack = replaceLabels(numStack, stack);
            }
            case Opcodes.F_APPEND -> local = replaceLabels(numLocal, local);
            case Opcodes.F_CHOP, Opcodes.F_SAME -> {}
            case Opcodes.F_SAME1 -> stack = replaceLabels(1, stack);
            default -> throw new AssertionError();
        }
        super.visitFrame(type, numLocal, local, numStack, stack);
    }

    private Object[] replaceLabels(int num, Object[] array) {
        Object[] result = array;
        for(int ix = 0; ix < num; ix++) {
            Label repl = translateForUninitialized.get(result[ix]);
            if(repl == null) continue;
            if(result == array) result = array.clone();
            result[ix] = repl;
        }
        return result;
    }

    private boolean instrumentReportCoverageInvocation() {
        int lineNumber = newLineNumber;
        if(lineNumber < 0) return false;
        newLineNumber = -1;
        super.visitFieldInsn(Opcodes.GETSTATIC,
             "java/lang/System", "out", "Ljava/io/PrintStream;");
        super.visitLdcInsn(slashClassName + "." + methodName + " line " + lineNumber);
        super.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
            "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        return true;
    }

    @Override
    public void visitLdcInsn(Object value) {
        instrumentReportCoverageInvocation();
        super.visitLdcInsn(value);
    }

    @Override
    public void visitInsn(int opcode) {
        instrumentReportCoverageInvocation();
        super.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        instrumentReportCoverageInvocation();
        super.visitIntInsn(opcode, operand);
    }

    @Override
    public void visitVarInsn(int opcode, int varIndex) {
        instrumentReportCoverageInvocation();
        super.visitVarInsn(opcode, varIndex);
    }

    @Override
    public void visitFieldInsn(int opcode,
        String owner, String name, String descriptor) {
        instrumentReportCoverageInvocation();
        super.visitFieldInsn(opcode, owner, name, descriptor);
    }

    @Override
    public void visitMethodInsn(int opcode,
        String owner, String name, String descriptor, boolean isInterface) {
        instrumentReportCoverageInvocation();
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }

    @Override
    public void visitInvokeDynamicInsn(
            String name, String descriptor, Handle bootstrapMethodHandle,
            Object... bootstrapMethodArguments) {
        instrumentReportCoverageInvocation();
        super.visitInvokeDynamicInsn(
            name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
    }

    @Override
    public void visitJumpInsn(int opcode, Label label) {
        instrumentReportCoverageInvocation();
        super.visitJumpInsn(opcode, label);
    }

    @Override
    public void visitIincInsn(int varIndex, int increment) {
        instrumentReportCoverageInvocation();
        super.visitIincInsn(varIndex, increment);
    }

    @Override
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        instrumentReportCoverageInvocation();
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }

    @Override
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        instrumentReportCoverageInvocation();
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }

    @Override
    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        instrumentReportCoverageInvocation();
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
    }

    @Override
    public AnnotationVisitor visitInsnAnnotation(
        int typeRef, TypePath tp, String desc, boolean visible) {
        instrumentReportCoverageInvocation();
        return super.visitInsnAnnotation(typeRef, tp, desc, visible);
    }
}

我用了一个直接的课堂访客

class CoverageClassVisitor extends ClassVisitor {
    private String className;

    CoverageClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public void visit(
        int version, int acc, String name, String sig, String superName, String[] ifs) {

        className = name;
        super.visit(version, acc, name, sig, superName, ifs);
    }
    @Override
    public MethodVisitor visitMethod(int access,
        String name, String descriptor, String signature, String[] exceptions) {
        return new CoverageMethodVisitor(
            super.visitMethod(access, name, descriptor, signature, exceptions),
            className,
            name);
    }
}

…以及以下测试代码

public class ReportLineNumbers {
    public static void main(String[] arg) throws IOException, IllegalAccessException {
        String className = ReportLineNumbers.class.getName() + "$Example";
//        ToolProvider.findFirst("javap")
//            .ifPresent(p -> p.run(System.out, System.err, "-c", className));
        ClassReader cr = new ClassReader(className);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        CoverageClassVisitor trans = new CoverageClassVisitor(cw);
        cr.accept(trans, 0);
        MethodHandles.lookup().defineClass(cw.toByteArray());
        Example.method();
    }
    static class Example {
        static void method() {
            for(int i = 0; i < 3; i++) {
                System.out.println(
                    i < 2?
                    new String(switch(i) {
                        case 0 -> {
//                            try {
                                yield "0";
//                            } catch(Throwable t) {
//                                yield "e";
//                            }
                        }
                        default -> {
                            for(int j = 1; j < 3; j++) {
                                System.out.println(j);
                            }
                            yield "3";
                        }
                    }):
                    new String(i == 2?
                        "4".toCharArray():
                        "5".toCharArray())
                );
            }
        }
    }
}

Example类的设计尽可能具有挑战性,涵盖NEW条指令前后的不同分支类型(正向、反向、切换).事实上,它是如此具有挑战性,以至于我不得不注释掉一个构造,就像Eclipse一样,即使没有转换器,它也会生成无效的字节码.但是当使用javac时,您可以启用这个内联try … catch …构造,以确保transformer甚至可以正确处理这个问题.

Java相关问答推荐

如果一个子类没有构造函数,超类也没有构造函数,那么为什么我可以构造子类的实例呢?

替换com. sun. jndi. dns. DnsContextFactory Wildfly23 JDK 17

工件部署期间出错[Tomcat 8.5.45]

缩小画布比例后更改滚动窗格的内部大小

使用Testcontainers与OpenLiberty Server进行集成测试会抛出SocketException

如何创建一个2d自上而下的移动系统,其中移动,同时持有两个关键是可能的处理?

如何确定springboot在将json字段转换为Dto时如何处理它?

如何正确创建序列图?

计算两个浮点数之间的距离是否对称?

测试容器无法加载类路径初始化脚本

我如何知道MediaDiscoverer何时完成发现介质?

Jolt变换JSON数组问题

如何读取3个CSV文件并在控制台中按顺序显示?(Java)

基于配置switch 的@Controller的条件摄取

Java 11 HttpCookie.parse在解析包含JSON的Cookie时引发IlLegalArgumentException

在Java中将.GRF转换为图像文件

如何使用Java对随机生成的字母数字优惠券代码进行过期设置

获取401未经授权,即使在标头中设置了浏览器名称和cookie

从 Java 17 切换回 Java 8 后出现的问题

为什么 JavaFX TextArea 中的 selectRange() 有时不突出显示所选内容?