You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASM Javaagent插桩报StackMapTable Uninitialized偏移错误

问题背景

我基于ASM框架结合Javaagent技术实现类字节码插桩,用于统计代码覆盖率(未使用JaCoCo的原因与本问题无关),核心实现逻辑为:当visitLineNumber方法被回调时,在后续待访问的第一条指令前插入方法调用字节码,记录当前命中的行号。

异常现象

按照上述逻辑实现插桩后,目标类加载时抛出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_delta=16)、偏移17(offset_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;
    }
}

上述代码中/** */的注释是我之前的猜测,不确定是否正确。


补充说明:
我此前误解了offset_delta = 0的含义,根据Java虚拟机规范说明:

栈映射帧对应的字节码偏移计算规则为:除非是方法的初始帧,否则当前帧适用的字节码偏移为前一帧的字节码偏移加上offset_delta再加1。

问题定位进展

错误原因初步判断

报错信息中的Uninitialized指向原字节码开头NEW指令生成的未初始化对象。原StackMapTable需要使用NEW指令前的标签(记为L0)来标识局部变量表和操作数栈中的未初始化对象,而插桩后L0对应的位置不再是NEW指令,因此抛出该错误。

可行解决思路

由于插桩后L0不再对应NEW指令,需要为该NEW指令创建新的标签,将两个栈映射帧中旧的L0标签替换为新标签。如果指定COMPUTE_FRAME参数,ASM会自动完成栈映射帧的重计算,但为了避免其他潜在问题我未开启该参数,因此需要手动修正栈映射帧。


内容的提问来源于stack exchange,提问作者Instein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 13:09:17